mirror of
https://github.com/zaphar/kitchen.git
synced 2025-07-22 19:40:14 -04:00
recipe categorization
This commit is contained in:
parent
3f1e79b001
commit
e02fcc82e1
@ -0,0 +1,2 @@
|
|||||||
|
-- Add down migration script here
|
||||||
|
alter table recipes drop column category;
|
@ -0,0 +1,2 @@
|
|||||||
|
-- Add up migration script here
|
||||||
|
alter table recipes add column category TEXT;
|
@ -1,5 +1,15 @@
|
|||||||
{
|
{
|
||||||
"db": "SQLite",
|
"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": {
|
"104f07472670436d3eee1733578bbf0c92dc4f965d3d13f9bf4bfbc92958c5b6": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"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"
|
"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": {
|
"19832e3582c05ed49c676fde33cde64274379a83a8dd130f6eec96c1d7250909": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"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"
|
"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": {
|
"23beb05e40cf011170182d4e98cdf1faa3d8df6e5956e471245e666f32e56962": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [],
|
"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"
|
"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": {
|
"describe": {
|
||||||
"columns": [],
|
"columns": [
|
||||||
"nullable": [],
|
{
|
||||||
|
"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": {
|
"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": {
|
"4237ff804f254c122a36a14135b90434c6576f48d3a83245503d702552ea9f30": {
|
||||||
"describe": {
|
"describe": {
|
||||||
@ -500,30 +536,6 @@
|
|||||||
},
|
},
|
||||||
"query": "delete from plan_recipes where user_id = ? and plan_date = ?"
|
"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": {
|
"9ad4acd9b9d32c9f9f441276aa71a17674fe4d65698848044778bd4aef77d42d": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [],
|
"columns": [],
|
||||||
|
@ -98,7 +98,7 @@ impl AsyncFileStore {
|
|||||||
let file_name = entry.file_name().to_string_lossy().to_string();
|
let file_name = entry.file_name().to_string_lossy().to_string();
|
||||||
debug!("adding recipe file {}", file_name);
|
debug!("adding recipe file {}", file_name);
|
||||||
let recipe_contents = read_to_string(entry.path()).await?;
|
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 {
|
} else {
|
||||||
warn!(
|
warn!(
|
||||||
file = %entry.path().to_string_lossy(),
|
file = %entry.path().to_string_lossy(),
|
||||||
@ -118,7 +118,11 @@ impl AsyncFileStore {
|
|||||||
if recipe_path.exists().await && recipe_path.is_file().await {
|
if recipe_path.exists().await && recipe_path.is_file().await {
|
||||||
debug!("Found recipe file {}", recipe_path.to_string_lossy());
|
debug!("Found recipe file {}", recipe_path.to_string_lossy());
|
||||||
let recipe_contents = read_to_string(recipe_path).await?;
|
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 {
|
} else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
@ -436,12 +436,13 @@ impl APIStore for SqliteStore {
|
|||||||
struct RecipeRow {
|
struct RecipeRow {
|
||||||
pub recipe_id: String,
|
pub recipe_id: String,
|
||||||
pub recipe_text: Option<String>,
|
pub recipe_text: Option<String>,
|
||||||
|
pub category: Option<String>,
|
||||||
}
|
}
|
||||||
let id = id.as_ref();
|
let id = id.as_ref();
|
||||||
let user_id = user_id.as_ref();
|
let user_id = user_id.as_ref();
|
||||||
let entry = sqlx::query_as!(
|
let entry = sqlx::query_as!(
|
||||||
RecipeRow,
|
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,
|
user_id,
|
||||||
id,
|
id,
|
||||||
)
|
)
|
||||||
@ -452,6 +453,7 @@ impl APIStore for SqliteStore {
|
|||||||
RecipeEntry(
|
RecipeEntry(
|
||||||
row.recipe_id.clone(),
|
row.recipe_id.clone(),
|
||||||
row.recipe_text.clone().unwrap_or_else(|| String::new()),
|
row.recipe_text.clone().unwrap_or_else(|| String::new()),
|
||||||
|
row.category.clone()
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.nth(0);
|
.nth(0);
|
||||||
@ -466,10 +468,11 @@ impl APIStore for SqliteStore {
|
|||||||
struct RecipeRow {
|
struct RecipeRow {
|
||||||
pub recipe_id: String,
|
pub recipe_id: String,
|
||||||
pub recipe_text: Option<String>,
|
pub recipe_text: Option<String>,
|
||||||
|
pub category: Option<String>,
|
||||||
}
|
}
|
||||||
let rows = sqlx::query_as!(
|
let rows = sqlx::query_as!(
|
||||||
RecipeRow,
|
RecipeRow,
|
||||||
"select recipe_id, recipe_text from recipes where user_id = ?",
|
"select recipe_id, recipe_text, category from recipes where user_id = ?",
|
||||||
user_id,
|
user_id,
|
||||||
)
|
)
|
||||||
.fetch_all(self.pool.as_ref())
|
.fetch_all(self.pool.as_ref())
|
||||||
@ -479,6 +482,7 @@ impl APIStore for SqliteStore {
|
|||||||
RecipeEntry(
|
RecipeEntry(
|
||||||
row.recipe_id.clone(),
|
row.recipe_id.clone(),
|
||||||
row.recipe_text.clone().unwrap_or_else(|| String::new()),
|
row.recipe_text.clone().unwrap_or_else(|| String::new()),
|
||||||
|
row.category.clone(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@ -493,12 +497,14 @@ impl APIStore for SqliteStore {
|
|||||||
for entry in recipes {
|
for entry in recipes {
|
||||||
let recipe_id = entry.recipe_id().to_owned();
|
let recipe_id = entry.recipe_id().to_owned();
|
||||||
let recipe_text = entry.recipe_text().to_owned();
|
let recipe_text = entry.recipe_text().to_owned();
|
||||||
|
let category = entry.category();
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"insert into recipes (user_id, recipe_id, recipe_text) values (?, ?, ?)
|
"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",
|
on conflict(user_id, recipe_id) do update set recipe_text=excluded.recipe_text, category=excluded.category",
|
||||||
user_id,
|
user_id,
|
||||||
recipe_id,
|
recipe_id,
|
||||||
recipe_text,
|
recipe_text,
|
||||||
|
category,
|
||||||
)
|
)
|
||||||
.execute(self.pool.as_ref())
|
.execute(self.pool.as_ref())
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -50,11 +50,11 @@ impl Mealplan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub struct RecipeEntry(pub String, pub String);
|
pub struct RecipeEntry(pub String, pub String, pub Option<String>);
|
||||||
|
|
||||||
impl RecipeEntry {
|
impl RecipeEntry {
|
||||||
pub fn new<IS: Into<String>, TS: Into<String>>(recipe_id: IS, text: TS) -> Self {
|
pub fn new<IS: Into<String>, TS: Into<String>>(recipe_id: IS, text: TS) -> Self {
|
||||||
Self(recipe_id.into(), text.into())
|
Self(recipe_id.into(), text.into(), None)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_recipe_id<S: Into<String>>(&mut self, id: S) {
|
pub fn set_recipe_id<S: Into<String>>(&mut self, id: S) {
|
||||||
@ -72,6 +72,14 @@ impl RecipeEntry {
|
|||||||
pub fn recipe_text(&self) -> &str {
|
pub fn recipe_text(&self) -> &str {
|
||||||
self.1.as_str()
|
self.1.as_str()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_category<S: Into<String>>(&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.
|
/// A Recipe with a title, description, and a series of steps.
|
||||||
|
@ -31,10 +31,17 @@ Instructions here
|
|||||||
#[component]
|
#[component]
|
||||||
pub fn AddRecipe<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View<G> {
|
pub fn AddRecipe<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View<G> {
|
||||||
let recipe_title = create_signal(cx, String::new());
|
let recipe_title = create_signal(cx, String::new());
|
||||||
|
let category = create_signal(cx, String::new());
|
||||||
let create_recipe_signal = create_signal(cx, ());
|
let create_recipe_signal = create_signal(cx, ());
|
||||||
let dirty = create_signal(cx, false);
|
let dirty = create_signal(cx, false);
|
||||||
|
|
||||||
let entry = create_memo(cx, || {
|
let entry = create_memo(cx, || {
|
||||||
|
let category = category.get().as_ref().to_owned();
|
||||||
|
let category = if category.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(category)
|
||||||
|
};
|
||||||
RecipeEntry(
|
RecipeEntry(
|
||||||
recipe_title
|
recipe_title
|
||||||
.get()
|
.get()
|
||||||
@ -45,6 +52,7 @@ pub fn AddRecipe<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View
|
|||||||
STARTER_RECIPE
|
STARTER_RECIPE
|
||||||
.replace("TITLE_PLACEHOLDER", recipe_title.get().as_str())
|
.replace("TITLE_PLACEHOLDER", recipe_title.get().as_str())
|
||||||
.replace("\r", ""),
|
.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 |_| {
|
input(bind:value=recipe_title, type="text", name="recipe_title", id="recipe_title", on:change=move |_| {
|
||||||
dirty.set(true);
|
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 |_| {
|
button(on:click=move |_| {
|
||||||
create_recipe_signal.trigger_subscribers();
|
create_recipe_signal.trigger_subscribers();
|
||||||
if !*dirty.get_untracked() {
|
if !*dirty.get_untracked() {
|
||||||
|
@ -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 id = create_memo(cx, || recipe.get().recipe_id().to_owned());
|
||||||
let dirty = create_signal(cx, false);
|
let dirty = create_signal(cx, false);
|
||||||
let ts = create_signal(cx, js_lib::get_ms_timestamp());
|
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");
|
debug!("creating editor view");
|
||||||
view! {cx,
|
view! {cx,
|
||||||
|
label(for="recipe_category") { "Category" }
|
||||||
|
input(name="recipe_category", bind:value=category, on:change=move |_| dirty.set(true))
|
||||||
div(class="grid") {
|
div(class="grid") {
|
||||||
textarea(bind:value=text, aria-invalid=aria_hint.get(), rows=20, on:change=move |_| {
|
div {
|
||||||
dirty.set(true);
|
label(for="recipe_text") { "Recipe" }
|
||||||
check_recipe_parses(text.get_untracked().as_str(), error_text, aria_hint);
|
textarea(name="recipe_text", bind:value=text, aria-invalid=aria_hint.get(), rows=20, on:change=move |_| {
|
||||||
}, on:input=move |_| {
|
dirty.set(true);
|
||||||
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);
|
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()) }
|
div(class="parse") { (error_text.get()) }
|
||||||
}
|
}
|
||||||
span(role="button", on:click=move |_| {
|
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");
|
debug!("Recipe text is changed");
|
||||||
spawn_local_scoped(cx, {
|
spawn_local_scoped(cx, {
|
||||||
let store = crate::api::HttpStore::get_from_context(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 {
|
async move {
|
||||||
debug!("Attempting to save recipe");
|
debug!("Attempting to save recipe");
|
||||||
if let Err(e) = store
|
if let Err(e) = store
|
||||||
.store_recipes(vec![RecipeEntry(
|
.store_recipes(vec![RecipeEntry(
|
||||||
id.get_untracked().as_ref().clone(),
|
id.get_untracked().as_ref().clone(),
|
||||||
text.get_untracked().as_ref().clone(),
|
text.get_untracked().as_ref().clone(),
|
||||||
|
category,
|
||||||
)])
|
)])
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user