mirror of
https://github.com/zaphar/kitchen.git
synced 2025-07-22 19:40:14 -04:00
Store staples seperately from recipes
This commit is contained in:
parent
35968ac78d
commit
c16b5bab7c
2
kitchen/migrations/20230106195850_staples.down.sql
Normal file
2
kitchen/migrations/20230106195850_staples.down.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
-- Add down migration script here
|
||||||
|
drop table staples;
|
6
kitchen/migrations/20230106195850_staples.up.sql
Normal file
6
kitchen/migrations/20230106195850_staples.up.sql
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
-- Add up migration script here
|
||||||
|
create table staples (
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
primary key(user_id)
|
||||||
|
);
|
@ -112,6 +112,16 @@
|
|||||||
},
|
},
|
||||||
"query": "select plan_date as \"plan_date: NaiveDate\", recipe_id, count\nfrom plan_recipes\nwhere\n user_id = ?\n and date(plan_date) > ?\norder by user_id, plan_date"
|
"query": "select plan_date as \"plan_date: NaiveDate\", recipe_id, count\nfrom plan_recipes\nwhere\n user_id = ?\n and date(plan_date) > ?\norder by user_id, plan_date"
|
||||||
},
|
},
|
||||||
|
"1b4a7250e451991ee7e642c6389656814e0dd00c94e59383c02af6313bc76213": {
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"nullable": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"query": "insert into staples (user_id, content) values (?, ?)\n on conflict(user_id) do update set content = excluded.content"
|
||||||
|
},
|
||||||
"2582522f8ca9f12eccc70a3b339d9030aee0f52e62d6674cfd3862de2a68a177": {
|
"2582522f8ca9f12eccc70a3b339d9030aee0f52e62d6674cfd3862de2a68a177": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [],
|
"columns": [],
|
||||||
@ -212,6 +222,24 @@
|
|||||||
},
|
},
|
||||||
"query": "insert into users (id, password_hashed) values (?, ?)"
|
"query": "insert into users (id, password_hashed) values (?, ?)"
|
||||||
},
|
},
|
||||||
|
"64af3f713eb4c61ac02cab2dfea83d0ed197e602e99079d4d32cb38d677edf2e": {
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "content",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nullable": [
|
||||||
|
false
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"query": "select content from staples where user_id = ?"
|
||||||
|
},
|
||||||
"6e28698330e42fd6c87ba1e6f1deb664c0d3995caa2b937ceac8c908e98aded6": {
|
"6e28698330e42fd6c87ba1e6f1deb664c0d3995caa2b937ceac8c908e98aded6": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [],
|
"columns": [],
|
||||||
|
@ -28,7 +28,7 @@ use recipes::{IngredientKey, RecipeEntry};
|
|||||||
use rust_embed::RustEmbed;
|
use rust_embed::RustEmbed;
|
||||||
use tower::ServiceBuilder;
|
use tower::ServiceBuilder;
|
||||||
use tower_http::trace::TraceLayer;
|
use tower_http::trace::TraceLayer;
|
||||||
use tracing::{debug, info, instrument};
|
use tracing::{debug, error, info, instrument};
|
||||||
|
|
||||||
use client_api as api;
|
use client_api as api;
|
||||||
use storage::{APIStore, AuthStore};
|
use storage::{APIStore, AuthStore};
|
||||||
@ -383,6 +383,49 @@ async fn api_user_account(session: storage::UserIdFromSession) -> api::AccountRe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn api_staples(
|
||||||
|
Extension(app_store): Extension<Arc<storage::SqliteStore>>,
|
||||||
|
session: storage::UserIdFromSession,
|
||||||
|
) -> api::Response<Option<String>> {
|
||||||
|
use storage::{UserId, UserIdFromSession::FoundUserId};
|
||||||
|
if let FoundUserId(UserId(user_id)) = session {
|
||||||
|
match app_store.fetch_staples(user_id).await {
|
||||||
|
Ok(staples) => api::Response::success(staples),
|
||||||
|
Err(err) => {
|
||||||
|
error!(?err);
|
||||||
|
api::Response::error(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
|
||||||
|
format!("{:?}", err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
api::Response::Unauthorized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn api_save_staples(
|
||||||
|
Extension(app_store): Extension<Arc<storage::SqliteStore>>,
|
||||||
|
session: storage::UserIdFromSession,
|
||||||
|
Json(content): Json<String>,
|
||||||
|
) -> api::Response<()> {
|
||||||
|
use storage::{UserId, UserIdFromSession::FoundUserId};
|
||||||
|
if let FoundUserId(UserId(user_id)) = session {
|
||||||
|
match app_store.save_staples(user_id, content).await {
|
||||||
|
Ok(_) => api::EmptyResponse::success(()),
|
||||||
|
Err(err) => {
|
||||||
|
error!(?err);
|
||||||
|
api::EmptyResponse::error(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
|
||||||
|
format!("{:?}", err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
api::EmptyResponse::Unauthorized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn mk_v1_routes() -> Router {
|
fn mk_v1_routes() -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/recipes", get(api_recipes).post(api_save_recipes))
|
.route("/recipes", get(api_recipes).post(api_save_recipes))
|
||||||
@ -416,6 +459,7 @@ fn mk_v2_routes() -> Router {
|
|||||||
"/category_map",
|
"/category_map",
|
||||||
get(api_category_mappings).post(api_save_category_mappings),
|
get(api_category_mappings).post(api_save_category_mappings),
|
||||||
)
|
)
|
||||||
|
.route("/staples", get(api_staples).post(api_save_staples))
|
||||||
// All the routes above require a UserId.
|
// All the routes above require a UserId.
|
||||||
.route("/auth", get(auth::handler).post(auth::handler))
|
.route("/auth", get(auth::handler).post(auth::handler))
|
||||||
.route("/account", get(api_user_account))
|
.route("/account", get(api_user_account))
|
||||||
|
1
kitchen/src/web/storage/fetch_staples.sql
Normal file
1
kitchen/src/web/storage/fetch_staples.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
select content from staples where user_id = ?
|
@ -148,6 +148,10 @@ pub trait APIStore {
|
|||||||
modified_amts: BTreeMap<IngredientKey, String>,
|
modified_amts: BTreeMap<IngredientKey, String>,
|
||||||
extra_items: Vec<(String, String)>,
|
extra_items: Vec<(String, String)>,
|
||||||
) -> Result<()>;
|
) -> Result<()>;
|
||||||
|
|
||||||
|
async fn fetch_staples<S: AsRef<str> + Send>(&self, user_id: S) -> Result<Option<String>>;
|
||||||
|
|
||||||
|
async fn save_staples<S: AsRef<str> + Send>(&self, user_id: S, content: S) -> Result<()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@ -694,4 +698,24 @@ impl APIStore for SqliteStore {
|
|||||||
transaction.commit().await?;
|
transaction.commit().await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn save_staples<S: AsRef<str> + Send>(&self, user_id: S, content: S) -> Result<()> {
|
||||||
|
let (user_id, content) = (user_id.as_ref(), content.as_ref());
|
||||||
|
sqlx::query_file!("src/web/storage/save_staples.sql", user_id, content)
|
||||||
|
.execute(self.pool.as_ref())
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch_staples<S: AsRef<str> + Send>(&self, user_id: S) -> Result<Option<String>> {
|
||||||
|
let user_id = user_id.as_ref();
|
||||||
|
if let Some(content) =
|
||||||
|
sqlx::query_file_scalar!("src/web/storage/fetch_staples.sql", user_id)
|
||||||
|
.fetch_optional(self.pool.as_ref())
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
return Ok(Some(content));
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
2
kitchen/src/web/storage/save_staples.sql
Normal file
2
kitchen/src/web/storage/save_staples.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
insert into staples (user_id, content) values (?, ?)
|
||||||
|
on conflict(user_id) do update set content = excluded.content
|
@ -135,12 +135,17 @@ impl IngredientAccumulator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn accumulate_from(&mut self, r: &Recipe) {
|
pub fn accumulate_ingredients_for<'a, Iter, S>(&'a mut self, recipe_title: S, ingredients: Iter)
|
||||||
for i in r.steps.iter().map(|s| s.ingredients.iter()).flatten() {
|
where
|
||||||
|
Iter: Iterator<Item = &'a Ingredient>,
|
||||||
|
S: Into<String>,
|
||||||
|
{
|
||||||
|
let recipe_title = recipe_title.into();
|
||||||
|
for i in ingredients {
|
||||||
let key = i.key();
|
let key = i.key();
|
||||||
if !self.inner.contains_key(&key) {
|
if !self.inner.contains_key(&key) {
|
||||||
let mut set = BTreeSet::new();
|
let mut set = BTreeSet::new();
|
||||||
set.insert(r.title.clone());
|
set.insert(recipe_title.clone());
|
||||||
self.inner.insert(key, (i.clone(), set));
|
self.inner.insert(key, (i.clone(), set));
|
||||||
} else {
|
} else {
|
||||||
let amt = match (self.inner[&key].0.amt, i.amt) {
|
let amt = match (self.inner[&key].0.amt, i.amt) {
|
||||||
@ -151,12 +156,19 @@ impl IngredientAccumulator {
|
|||||||
};
|
};
|
||||||
self.inner.get_mut(&key).map(|(i, set)| {
|
self.inner.get_mut(&key).map(|(i, set)| {
|
||||||
i.amt = amt;
|
i.amt = amt;
|
||||||
set.insert(r.title.clone());
|
set.insert(recipe_title.clone());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn accumulate_from(&mut self, r: &Recipe) {
|
||||||
|
self.accumulate_ingredients_for(
|
||||||
|
&r.title,
|
||||||
|
r.steps.iter().map(|s| s.ingredients.iter()).flatten(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn ingredients(self) -> BTreeMap<IngredientKey, (Ingredient, BTreeSet<String>)> {
|
pub fn ingredients(self) -> BTreeMap<IngredientKey, (Ingredient, BTreeSet<String>)> {
|
||||||
self.inner
|
self.inner
|
||||||
}
|
}
|
||||||
|
@ -59,6 +59,14 @@ pub fn as_measure(i: &str) -> std::result::Result<Measure, String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn as_ingredient_list(i: &str) -> std::result::Result<Vec<Ingredient>, String> {
|
||||||
|
match ingredient_list(StrIter::new(i)) {
|
||||||
|
Result::Abort(e) | Result::Fail(e) => Err(format_err(e)),
|
||||||
|
Result::Incomplete(_) => Err(format!("Incomplete categories list can not parse")),
|
||||||
|
Result::Complete(_, m) => Ok(m),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
make_fn!(
|
make_fn!(
|
||||||
pub categories<StrIter, BTreeMap<String, String>>,
|
pub categories<StrIter, BTreeMap<String, String>>,
|
||||||
do_each!(
|
do_each!(
|
||||||
|
@ -17,10 +17,10 @@ use base64;
|
|||||||
use reqwasm;
|
use reqwasm;
|
||||||
use serde_json::{from_str, to_string};
|
use serde_json::{from_str, to_string};
|
||||||
use sycamore::prelude::*;
|
use sycamore::prelude::*;
|
||||||
use tracing::{debug, error, instrument, warn};
|
use tracing::{debug, error, instrument};
|
||||||
|
|
||||||
use client_api::*;
|
use client_api::*;
|
||||||
use recipes::{parse, IngredientKey, Recipe, RecipeEntry};
|
use recipes::{IngredientKey, RecipeEntry};
|
||||||
use wasm_bindgen::JsValue;
|
use wasm_bindgen::JsValue;
|
||||||
use web_sys::Storage;
|
use web_sys::Storage;
|
||||||
|
|
||||||
@ -310,6 +310,18 @@ impl LocalStore {
|
|||||||
)
|
)
|
||||||
.expect("Failed to set inventory");
|
.expect("Failed to set inventory");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_staples(&self, content: &String) {
|
||||||
|
self.store
|
||||||
|
.set("staples", content)
|
||||||
|
.expect("Failed to set staples in local store");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_staples(&self) -> Option<String> {
|
||||||
|
self.store
|
||||||
|
.get("staples")
|
||||||
|
.expect("Failed to retreive staples from local store")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@ -646,4 +658,41 @@ impl HttpStore {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn fetch_staples(&self) -> Result<Option<String>, Error> {
|
||||||
|
let mut path = self.v2_path();
|
||||||
|
path.push_str("/staples");
|
||||||
|
let resp = reqwasm::http::Request::get(&path).send().await?;
|
||||||
|
if resp.status() != 200 {
|
||||||
|
debug!("Invalid response back");
|
||||||
|
Err(format!("Status: {}", resp.status()).into())
|
||||||
|
} else {
|
||||||
|
Ok(resp
|
||||||
|
.json::<Response<Option<String>>>()
|
||||||
|
.await
|
||||||
|
.expect("Failed to parse staples json")
|
||||||
|
.as_success()
|
||||||
|
.unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn store_staples<S: AsRef<str>>(&self, content: S) -> Result<(), Error> {
|
||||||
|
let mut path = self.v2_path();
|
||||||
|
path.push_str("/staples");
|
||||||
|
let serialized_staples: String =
|
||||||
|
to_string(content.as_ref()).expect("Failed to serialize staples to json");
|
||||||
|
|
||||||
|
let resp = reqwasm::http::Request::post(&path)
|
||||||
|
.body(&serialized_staples)
|
||||||
|
.header("content-type", "application/json")
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
if resp.status() != 200 {
|
||||||
|
debug!("Invalid response back");
|
||||||
|
Err(format!("Status: {}", resp.status()).into())
|
||||||
|
} else {
|
||||||
|
debug!("We got a valid response back!");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use client_api::UserData;
|
use client_api::UserData;
|
||||||
use recipes::{parse, IngredientKey, Recipe, RecipeEntry};
|
use recipes::{parse, Ingredient, IngredientKey, Recipe, RecipeEntry};
|
||||||
use sycamore::futures::spawn_local_scoped;
|
use sycamore::futures::spawn_local_scoped;
|
||||||
use sycamore::prelude::*;
|
use sycamore::prelude::*;
|
||||||
use sycamore_state::{Handler, MessageMapper};
|
use sycamore_state::{Handler, MessageMapper};
|
||||||
@ -30,7 +30,7 @@ use crate::api::{HttpStore, LocalStore};
|
|||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub recipe_counts: BTreeMap<String, usize>,
|
pub recipe_counts: BTreeMap<String, usize>,
|
||||||
pub extras: Vec<(String, String)>,
|
pub extras: Vec<(String, String)>,
|
||||||
pub staples: Option<Recipe>,
|
pub staples: Option<BTreeSet<Ingredient>>,
|
||||||
pub recipes: BTreeMap<String, Recipe>,
|
pub recipes: BTreeMap<String, Recipe>,
|
||||||
pub category_map: BTreeMap<String, String>,
|
pub category_map: BTreeMap<String, String>,
|
||||||
pub filtered_ingredients: BTreeSet<IngredientKey>,
|
pub filtered_ingredients: BTreeSet<IngredientKey>,
|
||||||
@ -69,6 +69,7 @@ pub enum Message {
|
|||||||
SetUserData(UserData),
|
SetUserData(UserData),
|
||||||
SaveState(Option<Box<dyn FnOnce()>>),
|
SaveState(Option<Box<dyn FnOnce()>>),
|
||||||
LoadState(Option<Box<dyn FnOnce()>>),
|
LoadState(Option<Box<dyn FnOnce()>>),
|
||||||
|
UpdateStaples(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for Message {
|
impl Debug for Message {
|
||||||
@ -108,6 +109,7 @@ impl Debug for Message {
|
|||||||
Self::SetUserData(arg0) => f.debug_tuple("SetUserData").field(arg0).finish(),
|
Self::SetUserData(arg0) => f.debug_tuple("SetUserData").field(arg0).finish(),
|
||||||
Self::SaveState(_) => write!(f, "SaveState"),
|
Self::SaveState(_) => write!(f, "SaveState"),
|
||||||
Self::LoadState(_) => write!(f, "LoadState"),
|
Self::LoadState(_) => write!(f, "LoadState"),
|
||||||
|
Self::UpdateStaples(arg) => f.debug_tuple("UpdateStaples").field(arg).finish(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -158,11 +160,25 @@ impl StateMachine {
|
|||||||
let mut state = original.get().as_ref().clone();
|
let mut state = original.get().as_ref().clone();
|
||||||
info!("Synchronizing Recipes");
|
info!("Synchronizing Recipes");
|
||||||
let recipe_entries = &store.fetch_recipes().await?;
|
let recipe_entries = &store.fetch_recipes().await?;
|
||||||
let (staples, recipes) = filter_recipes(&recipe_entries)?;
|
let (_old_staples, recipes) = filter_recipes(&recipe_entries)?;
|
||||||
if let Some(recipes) = recipes {
|
if let Some(recipes) = recipes {
|
||||||
state.staples = staples;
|
|
||||||
state.recipes = recipes;
|
state.recipes = recipes;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
state.staples = if let Some(content) = store.fetch_staples().await? {
|
||||||
|
local_store.set_staples(&content);
|
||||||
|
// now we need to parse staples as ingredients
|
||||||
|
let mut staples = parse::as_ingredient_list(&content)?;
|
||||||
|
Some(staples.drain(0..).collect())
|
||||||
|
} else {
|
||||||
|
if let Some(content) = local_store.get_staples() {
|
||||||
|
let mut staples = parse::as_ingredient_list(&content)?;
|
||||||
|
Some(staples.drain(0..).collect())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if let Some(recipe_entries) = recipe_entries {
|
if let Some(recipe_entries) = recipe_entries {
|
||||||
local_store.set_all_recipes(recipe_entries);
|
local_store.set_all_recipes(recipe_entries);
|
||||||
}
|
}
|
||||||
@ -393,6 +409,18 @@ impl MessageMapper<Message, AppState> for StateMachine {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
Message::UpdateStaples(content) => {
|
||||||
|
let store = self.store.clone();
|
||||||
|
let local_store = self.local_store.clone();
|
||||||
|
spawn_local_scoped(cx, async move {
|
||||||
|
local_store.set_staples(&content);
|
||||||
|
store
|
||||||
|
.store_staples(content)
|
||||||
|
.await
|
||||||
|
.expect("Failed to store staples");
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
original.set(original_copy);
|
original.set(original_copy);
|
||||||
}
|
}
|
||||||
|
@ -104,7 +104,7 @@ pub fn Categories<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> Vie
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(staples) = &state.staples {
|
if let Some(staples) = &state.staples {
|
||||||
for (_, i) in staples.get_ingredients().iter() {
|
for i in staples.iter() {
|
||||||
let ingredient_name = i.name.clone();
|
let ingredient_name = i.name.clone();
|
||||||
ingredients
|
ingredients
|
||||||
.entry(ingredient_name)
|
.entry(ingredient_name)
|
||||||
@ -125,7 +125,7 @@ pub fn Categories<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> Vie
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(staples) = &state.staples {
|
if let Some(staples) = &state.staples {
|
||||||
for (_, i) in staples.get_ingredients().iter() {
|
for i in staples.iter() {
|
||||||
ingredients.insert(i.name.clone());
|
ingredients.insert(i.name.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ pub mod recipe_list;
|
|||||||
pub mod recipe_plan;
|
pub mod recipe_plan;
|
||||||
pub mod recipe_selection;
|
pub mod recipe_selection;
|
||||||
pub mod shopping_list;
|
pub mod shopping_list;
|
||||||
|
pub mod staples;
|
||||||
pub mod tabs;
|
pub mod tabs;
|
||||||
|
|
||||||
pub use add_recipe::*;
|
pub use add_recipe::*;
|
||||||
@ -31,4 +32,5 @@ pub use recipe_list::*;
|
|||||||
pub use recipe_plan::*;
|
pub use recipe_plan::*;
|
||||||
pub use recipe_selection::*;
|
pub use recipe_selection::*;
|
||||||
pub use shopping_list::*;
|
pub use shopping_list::*;
|
||||||
|
pub use staples::*;
|
||||||
pub use tabs::*;
|
pub use tabs::*;
|
||||||
|
@ -43,7 +43,7 @@ fn make_ingredients_rows<'ctx, G: Html>(
|
|||||||
}
|
}
|
||||||
if *show_staples.get() {
|
if *show_staples.get() {
|
||||||
if let Some(staples) = &state.staples {
|
if let Some(staples) = &state.staples {
|
||||||
acc.accumulate_from(staples);
|
acc.accumulate_ingredients_for("Staples", staples.iter());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let mut ingredients = acc
|
let mut ingredients = acc
|
||||||
|
96
web/src/components/staples.rs
Normal file
96
web/src/components/staples.rs
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
// Copyright 2022 Jeremy Wall
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// 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 sycamore::{futures::spawn_local_scoped, prelude::*};
|
||||||
|
use tracing::{debug, error};
|
||||||
|
|
||||||
|
use crate::app_state::{Message, StateHandler};
|
||||||
|
use recipes::{self, parse};
|
||||||
|
|
||||||
|
fn check_ingredients_parses(
|
||||||
|
text: &str,
|
||||||
|
error_text: &Signal<String>,
|
||||||
|
aria_hint: &Signal<&'static str>,
|
||||||
|
) -> bool {
|
||||||
|
if let Err(e) = parse::as_ingredient_list(text) {
|
||||||
|
error!(?e, "Error parsing recipe");
|
||||||
|
error_text.set(e);
|
||||||
|
aria_hint.set("true");
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
error_text.set(String::from("No parse errors..."));
|
||||||
|
aria_hint.set("false");
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Props)]
|
||||||
|
pub struct IngredientComponentProps<'ctx> {
|
||||||
|
sh: StateHandler<'ctx>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn IngredientsEditor<'ctx, G: Html>(
|
||||||
|
cx: Scope<'ctx>,
|
||||||
|
props: IngredientComponentProps<'ctx>,
|
||||||
|
) -> View<G> {
|
||||||
|
let IngredientComponentProps { sh } = props;
|
||||||
|
let store = crate::api::HttpStore::get_from_context(cx);
|
||||||
|
let text = create_signal(cx, String::new());
|
||||||
|
let error_text = create_signal(cx, String::from("Parse results..."));
|
||||||
|
let aria_hint = create_signal(cx, "false");
|
||||||
|
|
||||||
|
spawn_local_scoped(cx, {
|
||||||
|
let store = store.clone();
|
||||||
|
async move {
|
||||||
|
let entry = store
|
||||||
|
.fetch_staples()
|
||||||
|
.await
|
||||||
|
.expect("Failure getting staples");
|
||||||
|
if let Some(entry) = entry {
|
||||||
|
check_ingredients_parses(entry.as_str(), error_text, aria_hint);
|
||||||
|
text.set(entry);
|
||||||
|
} else {
|
||||||
|
error_text.set("Unable to find staples".to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let dirty = create_signal(cx, false);
|
||||||
|
|
||||||
|
debug!("creating editor view");
|
||||||
|
view! {cx,
|
||||||
|
div(class="grid") {
|
||||||
|
textarea(bind:value=text, aria-invalid=aria_hint.get(), rows=20, on:change=move |_| {
|
||||||
|
dirty.set(true);
|
||||||
|
})
|
||||||
|
div(class="parse") { (error_text.get()) }
|
||||||
|
}
|
||||||
|
span(role="button", on:click=move |_| {
|
||||||
|
let unparsed = text.get();
|
||||||
|
check_ingredients_parses(unparsed.as_str(), error_text, aria_hint);
|
||||||
|
}) { "Check" } " "
|
||||||
|
span(role="button", on:click=move |_| {
|
||||||
|
let unparsed = text.get();
|
||||||
|
if !*dirty.get_untracked() {
|
||||||
|
debug!("Staples text is unchanged");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
debug!("triggering a save");
|
||||||
|
if check_ingredients_parses(unparsed.as_str(), error_text, aria_hint) {
|
||||||
|
debug!("Staples text is changed");
|
||||||
|
sh.dispatch(cx, Message::UpdateStaples(unparsed.as_ref().clone()));
|
||||||
|
}
|
||||||
|
}) { "Save" }
|
||||||
|
}
|
||||||
|
}
|
@ -12,7 +12,7 @@
|
|||||||
// 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 super::ManagePage;
|
use super::ManagePage;
|
||||||
use crate::{app_state::StateHandler, components::recipe::Editor};
|
use crate::{app_state::StateHandler, components::staples::IngredientsEditor};
|
||||||
|
|
||||||
use sycamore::prelude::*;
|
use sycamore::prelude::*;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
@ -23,6 +23,6 @@ pub fn StaplesPage<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> Vi
|
|||||||
view! {cx,
|
view! {cx,
|
||||||
ManagePage(
|
ManagePage(
|
||||||
selected=Some("Staples".to_owned()),
|
selected=Some("Staples".to_owned()),
|
||||||
) { Editor(recipe_id="staples.txt".to_owned(), sh=sh) }
|
) { IngredientsEditor(sh=sh) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user