diff --git a/kitchen/migrations/20230125164426_add_recipe_categories.down.sql b/kitchen/migrations/20230125164426_add_recipe_categories.down.sql new file mode 100644 index 0000000..7fe78ba --- /dev/null +++ b/kitchen/migrations/20230125164426_add_recipe_categories.down.sql @@ -0,0 +1,2 @@ +-- Add down migration script here +alter table recipes drop column category; diff --git a/kitchen/migrations/20230125164426_add_recipe_categories.up.sql b/kitchen/migrations/20230125164426_add_recipe_categories.up.sql new file mode 100644 index 0000000..760f9a3 --- /dev/null +++ b/kitchen/migrations/20230125164426_add_recipe_categories.up.sql @@ -0,0 +1,2 @@ +-- Add up migration script here +alter table recipes add column category TEXT; \ No newline at end of file diff --git a/kitchen/sqlx-data.json b/kitchen/sqlx-data.json index 17eba75..4ed433e 100644 --- a/kitchen/sqlx-data.json +++ b/kitchen/sqlx-data.json @@ -1,5 +1,15 @@ { "db": "SQLite", + "05a9f963e3f18b8ceb787c33b6dbdac993f999ff32bb5155f2dff8dc18d840bf": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 4 + } + }, + "query": "insert into recipes (user_id, recipe_id, recipe_text, category) values (?, ?, ?, ?)\n on conflict(user_id, recipe_id) do update set recipe_text=excluded.recipe_text, category=excluded.category" + }, "104f07472670436d3eee1733578bbf0c92dc4f965d3d13f9bf4bfbc92958c5b6": { "describe": { "columns": [ @@ -62,30 +72,6 @@ }, "query": "insert into filtered_ingredients(user_id, name, form, measure_type, plan_date)\n values (?, ?, ?, ?, date()) on conflict(user_id, name, form, measure_type, plan_date) DO NOTHING" }, - "196e289cbd65224293c4213552160a0cdf82f924ac597810fe05102e247b809d": { - "describe": { - "columns": [ - { - "name": "recipe_id", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "recipe_text", - "ordinal": 1, - "type_info": "Text" - } - ], - "nullable": [ - false, - true - ], - "parameters": { - "Right": 2 - } - }, - "query": "select recipe_id, recipe_text from recipes where user_id = ? and recipe_id = ?" - }, "19832e3582c05ed49c676fde33cde64274379a83a8dd130f6eec96c1d7250909": { "describe": { "columns": [ @@ -136,6 +122,36 @@ }, "query": "insert into modified_amts(user_id, name, form, measure_type, amt, plan_date)\n values (?, ?, ?, ?, ?, ?) on conflict (user_id, name, form, measure_type, plan_date) do update set amt=excluded.amt" }, + "1cc4412dfc3d4acdf257e839b50d6c9abbb6e74e7af606fd12da20f0aedde3de": { + "describe": { + "columns": [ + { + "name": "recipe_id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "recipe_text", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "category", + "ordinal": 2, + "type_info": "Text" + } + ], + "nullable": [ + false, + true, + true + ], + "parameters": { + "Right": 2 + } + }, + "query": "select recipe_id, recipe_text, category from recipes where user_id = ? and recipe_id = ?" + }, "23beb05e40cf011170182d4e98cdf1faa3d8df6e5956e471245e666f32e56962": { "describe": { "columns": [], @@ -256,15 +272,35 @@ }, "query": "with latest_dates as (\n select user_id, max(date(plan_date)) as plan_date from plan_recipes\n where user_id = ?\n group by user_id\n)\n\nselect\n modified_amts.name,\n modified_amts.form,\n modified_amts.measure_type,\n modified_amts.amt\nfrom latest_dates\ninner join modified_amts on\n latest_dates.user_id = modified_amts.user_id\n and latest_dates.plan_date = modified_amts.plan_date" }, - "3fd4017569dca4fe73e97e0e2bd612027a8c1b17b0b10faabd6459f56ca1c0bb": { + "40c589d8cb88d7ed723c8651833fe8541756ef0c57bf6296a4dfbda7d504dca8": { "describe": { - "columns": [], - "nullable": [], + "columns": [ + { + "name": "recipe_id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "recipe_text", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "category", + "ordinal": 2, + "type_info": "Text" + } + ], + "nullable": [ + false, + true, + true + ], "parameters": { - "Right": 3 + "Right": 1 } }, - "query": "insert into recipes (user_id, recipe_id, recipe_text) values (?, ?, ?)\n on conflict(user_id, recipe_id) do update set recipe_text=excluded.recipe_text" + "query": "select recipe_id, recipe_text, category from recipes where user_id = ?" }, "4237ff804f254c122a36a14135b90434c6576f48d3a83245503d702552ea9f30": { "describe": { @@ -500,30 +536,6 @@ }, "query": "delete from plan_recipes where user_id = ? and plan_date = ?" }, - "95fbc362a2e17add05218a2dac431275b5cc55bd7ac8f4173ee10afefceafa3b": { - "describe": { - "columns": [ - { - "name": "recipe_id", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "recipe_text", - "ordinal": 1, - "type_info": "Text" - } - ], - "nullable": [ - false, - true - ], - "parameters": { - "Right": 1 - } - }, - "query": "select recipe_id, recipe_text from recipes where user_id = ?" - }, "9ad4acd9b9d32c9f9f441276aa71a17674fe4d65698848044778bd4aef77d42d": { "describe": { "columns": [], diff --git a/kitchen/src/web/storage/file_store.rs b/kitchen/src/web/storage/file_store.rs index fc19df6..6c0305d 100644 --- a/kitchen/src/web/storage/file_store.rs +++ b/kitchen/src/web/storage/file_store.rs @@ -98,7 +98,7 @@ impl AsyncFileStore { let file_name = entry.file_name().to_string_lossy().to_string(); debug!("adding recipe file {}", file_name); let recipe_contents = read_to_string(entry.path()).await?; - entry_vec.push(RecipeEntry(file_name, recipe_contents)); + entry_vec.push(RecipeEntry(file_name, recipe_contents, None)); } else { warn!( file = %entry.path().to_string_lossy(), @@ -118,7 +118,11 @@ impl AsyncFileStore { if recipe_path.exists().await && recipe_path.is_file().await { debug!("Found recipe file {}", recipe_path.to_string_lossy()); let recipe_contents = read_to_string(recipe_path).await?; - return Ok(Some(RecipeEntry(id.as_ref().to_owned(), recipe_contents))); + return Ok(Some(RecipeEntry( + id.as_ref().to_owned(), + recipe_contents, + None, + ))); } else { return Ok(None); } diff --git a/kitchen/src/web/storage/mod.rs b/kitchen/src/web/storage/mod.rs index 8ae1dbf..d0a96e8 100644 --- a/kitchen/src/web/storage/mod.rs +++ b/kitchen/src/web/storage/mod.rs @@ -436,12 +436,13 @@ impl APIStore for SqliteStore { struct RecipeRow { pub recipe_id: String, pub recipe_text: Option, + pub category: Option, } let id = id.as_ref(); let user_id = user_id.as_ref(); let entry = sqlx::query_as!( RecipeRow, - "select recipe_id, recipe_text from recipes where user_id = ? and recipe_id = ?", + "select recipe_id, recipe_text, category from recipes where user_id = ? and recipe_id = ?", user_id, id, ) @@ -452,6 +453,7 @@ impl APIStore for SqliteStore { RecipeEntry( row.recipe_id.clone(), row.recipe_text.clone().unwrap_or_else(|| String::new()), + row.category.clone() ) }) .nth(0); @@ -466,10 +468,11 @@ impl APIStore for SqliteStore { struct RecipeRow { pub recipe_id: String, pub recipe_text: Option, + pub category: Option, } let rows = sqlx::query_as!( RecipeRow, - "select recipe_id, recipe_text from recipes where user_id = ?", + "select recipe_id, recipe_text, category from recipes where user_id = ?", user_id, ) .fetch_all(self.pool.as_ref()) @@ -479,6 +482,7 @@ impl APIStore for SqliteStore { RecipeEntry( row.recipe_id.clone(), row.recipe_text.clone().unwrap_or_else(|| String::new()), + row.category.clone(), ) }) .collect(); @@ -493,12 +497,14 @@ impl APIStore for SqliteStore { for entry in recipes { let recipe_id = entry.recipe_id().to_owned(); let recipe_text = entry.recipe_text().to_owned(); + let category = entry.category(); sqlx::query!( - "insert into recipes (user_id, recipe_id, recipe_text) values (?, ?, ?) - on conflict(user_id, recipe_id) do update set recipe_text=excluded.recipe_text", + "insert into recipes (user_id, recipe_id, recipe_text, category) values (?, ?, ?, ?) + on conflict(user_id, recipe_id) do update set recipe_text=excluded.recipe_text, category=excluded.category", user_id, recipe_id, recipe_text, + category, ) .execute(self.pool.as_ref()) .await?; diff --git a/recipes/src/lib.rs b/recipes/src/lib.rs index d674476..416b5dd 100644 --- a/recipes/src/lib.rs +++ b/recipes/src/lib.rs @@ -50,11 +50,11 @@ impl Mealplan { } #[derive(Serialize, Deserialize, Clone, Debug)] -pub struct RecipeEntry(pub String, pub String); +pub struct RecipeEntry(pub String, pub String, pub Option); impl RecipeEntry { pub fn new, TS: Into>(recipe_id: IS, text: TS) -> Self { - Self(recipe_id.into(), text.into()) + Self(recipe_id.into(), text.into(), None) } pub fn set_recipe_id>(&mut self, id: S) { @@ -72,6 +72,14 @@ impl RecipeEntry { pub fn recipe_text(&self) -> &str { self.1.as_str() } + + pub fn set_category>(&mut self, cat: S) { + self.2 = Some(cat.into()); + } + + pub fn category(&self) -> Option<&String> { + self.2.as_ref() + } } /// A Recipe with a title, description, and a series of steps. diff --git a/web/src/components/add_recipe.rs b/web/src/components/add_recipe.rs index 9827678..16d2717 100644 --- a/web/src/components/add_recipe.rs +++ b/web/src/components/add_recipe.rs @@ -31,10 +31,17 @@ Instructions here #[component] pub fn AddRecipe<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View { let recipe_title = create_signal(cx, String::new()); + let category = create_signal(cx, String::new()); let create_recipe_signal = create_signal(cx, ()); let dirty = create_signal(cx, false); let entry = create_memo(cx, || { + let category = category.get().as_ref().to_owned(); + let category = if category.is_empty() { + None + } else { + Some(category) + }; RecipeEntry( recipe_title .get() @@ -45,6 +52,7 @@ pub fn AddRecipe<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View STARTER_RECIPE .replace("TITLE_PLACEHOLDER", recipe_title.get().as_str()) .replace("\r", ""), + category, ) }); @@ -53,6 +61,9 @@ pub fn AddRecipe<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View input(bind:value=recipe_title, type="text", name="recipe_title", id="recipe_title", on:change=move |_| { dirty.set(true); }) + input(bind:value=category, type="text", name="recipe_title", id="recipe_title", on:change=move |_| { + dirty.set(true); + }) button(on:click=move |_| { create_recipe_signal.trigger_subscribers(); if !*dirty.get_untracked() { diff --git a/web/src/components/recipe.rs b/web/src/components/recipe.rs index f1abd4a..00bf12d 100644 --- a/web/src/components/recipe.rs +++ b/web/src/components/recipe.rs @@ -72,20 +72,26 @@ pub fn Editor<'ctx, G: Html>(cx: Scope<'ctx>, props: RecipeComponentProps<'ctx>) let id = create_memo(cx, || recipe.get().recipe_id().to_owned()); let dirty = create_signal(cx, false); let ts = create_signal(cx, js_lib::get_ms_timestamp()); + let category = create_signal(cx, recipe.get().category().cloned().unwrap_or_default()); debug!("creating editor view"); view! {cx, + label(for="recipe_category") { "Category" } + input(name="recipe_category", bind:value=category, on:change=move |_| dirty.set(true)) div(class="grid") { - textarea(bind:value=text, aria-invalid=aria_hint.get(), rows=20, on:change=move |_| { - dirty.set(true); - check_recipe_parses(text.get_untracked().as_str(), error_text, aria_hint); - }, on:input=move |_| { - let current_ts = js_lib::get_ms_timestamp(); - if (current_ts - *ts.get_untracked()) > 100 { + div { + label(for="recipe_text") { "Recipe" } + textarea(name="recipe_text", bind:value=text, aria-invalid=aria_hint.get(), rows=20, on:change=move |_| { + dirty.set(true); check_recipe_parses(text.get_untracked().as_str(), error_text, aria_hint); - ts.set(current_ts); - } - }) + }, on:input=move |_| { + let current_ts = js_lib::get_ms_timestamp(); + if (current_ts - *ts.get_untracked()) > 100 { + check_recipe_parses(text.get_untracked().as_str(), error_text, aria_hint); + ts.set(current_ts); + } + }) + } div(class="parse") { (error_text.get()) } } span(role="button", on:click=move |_| { @@ -99,12 +105,19 @@ pub fn Editor<'ctx, G: Html>(cx: Scope<'ctx>, props: RecipeComponentProps<'ctx>) debug!("Recipe text is changed"); spawn_local_scoped(cx, { let store = crate::api::HttpStore::get_from_context(cx); + let category = category.get_untracked(); + let category = if category.is_empty() { + None + } else { + Some(category.as_ref().clone()) + }; async move { debug!("Attempting to save recipe"); if let Err(e) = store .store_recipes(vec![RecipeEntry( id.get_untracked().as_ref().clone(), text.get_untracked().as_ref().clone(), + category, )]) .await {