recipe categorization

This commit is contained in:
Jeremy Wall 2023-01-24 17:46:47 -05:00
parent 3f1e79b001
commit e02fcc82e1
8 changed files with 128 additions and 70 deletions

View File

@ -0,0 +1,2 @@
-- Add down migration script here
alter table recipes drop column category;

View File

@ -0,0 +1,2 @@
-- Add up migration script here
alter table recipes add column category TEXT;

View File

@ -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": [],

View File

@ -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);
}

View File

@ -436,12 +436,13 @@ impl APIStore for SqliteStore {
struct RecipeRow {
pub recipe_id: String,
pub recipe_text: Option<String>,
pub category: Option<String>,
}
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<String>,
pub category: Option<String>,
}
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?;

View File

@ -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<String>);
impl RecipeEntry {
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) {
@ -72,6 +72,14 @@ impl RecipeEntry {
pub fn recipe_text(&self) -> &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.

View File

@ -31,10 +31,17 @@ Instructions here
#[component]
pub fn AddRecipe<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View<G> {
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() {

View File

@ -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
{