mirror of
https://github.com/zaphar/kitchen.git
synced 2025-07-22 19:40:14 -04:00
Implement a sycamore-state Handler for Kitchen
This commit is contained in:
parent
907af7f23c
commit
02536d63d8
8
Cargo.lock
generated
8
Cargo.lock
generated
@ -1344,6 +1344,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"sycamore",
|
||||
"sycamore-router",
|
||||
"sycamore-state",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"tracing-web",
|
||||
@ -2249,6 +2250,13 @@ dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sycamore-state"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"sycamore",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sycamore-web"
|
||||
version = "0.8.2"
|
||||
|
@ -103,7 +103,7 @@ pub type CategoryResponse = Response<String>;
|
||||
|
||||
pub type EmptyResponse = Response<()>;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct UserData {
|
||||
pub user_id: String,
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ crate-type = ["cdylib", "rlib"]
|
||||
[dependencies]
|
||||
recipes = { path = "../recipes" }
|
||||
client-api = { path = "../api", package="api", features = ["browser"] }
|
||||
sycamore-state = { path = "../../sycamore-state"}
|
||||
# This makes debugging panics more tractable.
|
||||
console_error_panic_hook = "0.1.7"
|
||||
serde_json = "1.0.79"
|
||||
|
@ -17,13 +17,17 @@ use base64;
|
||||
use reqwasm;
|
||||
use serde_json::{from_str, to_string};
|
||||
use sycamore::prelude::*;
|
||||
use sycamore_state::Handler;
|
||||
use tracing::{debug, error, info, instrument, warn};
|
||||
|
||||
use client_api::*;
|
||||
use recipes::{parse, IngredientKey, Recipe, RecipeEntry};
|
||||
use wasm_bindgen::JsValue;
|
||||
|
||||
use crate::{app_state, js_lib};
|
||||
use crate::{
|
||||
app_state::{self, AppState, Message, StateMachine},
|
||||
js_lib,
|
||||
};
|
||||
|
||||
#[instrument]
|
||||
fn filter_recipes(
|
||||
@ -53,6 +57,87 @@ fn filter_recipes(
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn init_app_state<'ctx>(
|
||||
cx: Scope<'ctx>,
|
||||
h: &'ctx Handler<'ctx, StateMachine, AppState, Message>,
|
||||
) {
|
||||
info!("Synchronizing Recipes");
|
||||
let store = HttpStore::get_from_context(cx);
|
||||
// TODO(jwall): Make our caching logic using storage more robust.
|
||||
let recipe_entries = match store.get_recipes().await {
|
||||
Ok(recipe_entries) => {
|
||||
if let Ok((staples, recipes)) = filter_recipes(&recipe_entries) {
|
||||
h.dispatch(Message::SetStaples(staples));
|
||||
if let Some(recipes) = recipes {
|
||||
h.dispatch(Message::InitRecipes(recipes));
|
||||
}
|
||||
}
|
||||
recipe_entries
|
||||
}
|
||||
Err(err) => {
|
||||
error!(?err);
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
if let Ok(Some(plan)) = store.get_plan().await {
|
||||
// set the counts.
|
||||
let mut plan_map = BTreeMap::new();
|
||||
for (id, count) in plan {
|
||||
plan_map.insert(id, count as usize);
|
||||
}
|
||||
h.dispatch(Message::InitRecipeCounts(plan_map));
|
||||
} else {
|
||||
// Initialize things to zero
|
||||
if let Some(rs) = recipe_entries {
|
||||
for r in rs {
|
||||
h.dispatch(Message::UpdateRecipeCount(r.recipe_id().to_owned(), 0))
|
||||
}
|
||||
}
|
||||
}
|
||||
info!("Checking for user_data in local storage");
|
||||
let storage = js_lib::get_storage();
|
||||
let user_data = storage
|
||||
.get("user_data")
|
||||
.expect("Couldn't read from storage");
|
||||
if let Some(data) = user_data {
|
||||
if let Ok(user_data) = from_str(&data) {
|
||||
h.dispatch(Message::SetUserData(user_data));
|
||||
}
|
||||
}
|
||||
info!("Synchronizing categories");
|
||||
match store.get_categories().await {
|
||||
Ok(Some(categories_content)) => {
|
||||
debug!(categories=?categories_content);
|
||||
match recipes::parse::as_categories(&categories_content) {
|
||||
Ok(category_map) => {
|
||||
h.dispatch(Message::SetCategoryMap(category_map));
|
||||
}
|
||||
Err(err) => {
|
||||
error!(?err)
|
||||
}
|
||||
};
|
||||
}
|
||||
Ok(None) => {
|
||||
warn!("There is no category file");
|
||||
}
|
||||
Err(e) => {
|
||||
error!("{:?}", e);
|
||||
}
|
||||
}
|
||||
info!("Synchronizing inventory data");
|
||||
match store.get_inventory_data().await {
|
||||
Ok((filtered_ingredients, modified_amts, extra_items)) => {
|
||||
h.dispatch(Message::InitAmts(modified_amts));
|
||||
h.dispatch(Message::InitFilteredIngredient(filtered_ingredients));
|
||||
h.dispatch(Message::InitExtras(BTreeSet::from_iter(extra_items)));
|
||||
}
|
||||
Err(e) => {
|
||||
error!("{:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(state))]
|
||||
pub async fn init_page_state(store: &HttpStore, state: &app_state::State) -> Result<(), String> {
|
||||
info!("Synchronizing Recipes");
|
||||
|
@ -19,6 +19,124 @@ use tracing::{debug, instrument, warn};
|
||||
use client_api::UserData;
|
||||
use recipes::{Ingredient, IngredientAccumulator, IngredientKey, Recipe};
|
||||
|
||||
use sycamore_state::{Handler, MessageMapper};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct AppState {
|
||||
pub recipe_counts: BTreeMap<String, usize>,
|
||||
pub extras: BTreeSet<(String, String)>,
|
||||
pub staples: Option<Recipe>,
|
||||
pub recipes: BTreeMap<String, Recipe>,
|
||||
pub category_map: BTreeMap<String, String>,
|
||||
pub filtered_ingredients: BTreeSet<IngredientKey>,
|
||||
pub modified_amts: BTreeMap<IngredientKey, String>,
|
||||
pub auth: Option<UserData>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
recipe_counts: BTreeMap::new(),
|
||||
extras: BTreeSet::new(),
|
||||
staples: None,
|
||||
recipes: BTreeMap::new(),
|
||||
category_map: BTreeMap::new(),
|
||||
filtered_ingredients: BTreeSet::new(),
|
||||
modified_amts: BTreeMap::new(),
|
||||
auth: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Message {
|
||||
InitRecipeCounts(BTreeMap<String, usize>),
|
||||
UpdateRecipeCount(String, usize),
|
||||
InitExtras(BTreeSet<(String, String)>),
|
||||
AddExtra(String, String),
|
||||
RemoveExtra(String, String),
|
||||
InitRecipes(BTreeMap<String, Recipe>),
|
||||
SetRecipe(String, Recipe),
|
||||
RemoveRecipe(String),
|
||||
SetStaples(Option<Recipe>),
|
||||
SetCategoryMap(BTreeMap<String, String>),
|
||||
InitFilteredIngredient(BTreeSet<IngredientKey>),
|
||||
AddFilteredIngredient(IngredientKey),
|
||||
RemoveFilteredIngredient(IngredientKey),
|
||||
InitAmts(BTreeMap<IngredientKey, String>),
|
||||
UpdateAmt(IngredientKey, String),
|
||||
SetUserData(UserData),
|
||||
UnsetUserData,
|
||||
}
|
||||
|
||||
// TODO(jwall): Add HttpStore here and do the proper things for each event.
|
||||
pub struct StateMachine();
|
||||
|
||||
impl MessageMapper<Message, AppState> for StateMachine {
|
||||
fn map(&self, msg: Message, original: &ReadSignal<AppState>) -> AppState {
|
||||
let mut original_copy = original.get().as_ref().clone();
|
||||
match msg {
|
||||
Message::InitRecipeCounts(map) => {
|
||||
original_copy.recipe_counts = map;
|
||||
}
|
||||
Message::UpdateRecipeCount(id, count) => {
|
||||
original_copy.recipe_counts.insert(id, count);
|
||||
}
|
||||
Message::InitExtras(set) => {
|
||||
original_copy.extras = set;
|
||||
}
|
||||
Message::AddExtra(amt, name) => {
|
||||
original_copy.extras.insert((amt, name));
|
||||
}
|
||||
Message::RemoveExtra(amt, name) => {
|
||||
original_copy.extras.remove(&(amt, name));
|
||||
}
|
||||
Message::SetStaples(staples) => {
|
||||
original_copy.staples = staples;
|
||||
}
|
||||
Message::InitRecipes(recipes) => {
|
||||
original_copy.recipes = recipes;
|
||||
}
|
||||
Message::SetRecipe(id, recipe) => {
|
||||
original_copy.recipes.insert(id, recipe);
|
||||
}
|
||||
Message::RemoveRecipe(id) => {
|
||||
original_copy.recipes.remove(&id);
|
||||
}
|
||||
Message::SetCategoryMap(map) => {
|
||||
original_copy.category_map = map;
|
||||
}
|
||||
Message::InitFilteredIngredient(set) => {
|
||||
original_copy.filtered_ingredients = set;
|
||||
}
|
||||
Message::AddFilteredIngredient(key) => {
|
||||
original_copy.filtered_ingredients.insert(key);
|
||||
}
|
||||
Message::RemoveFilteredIngredient(key) => {
|
||||
original_copy.filtered_ingredients.remove(&key);
|
||||
}
|
||||
Message::InitAmts(map) => {
|
||||
original_copy.modified_amts = map;
|
||||
}
|
||||
Message::UpdateAmt(key, amt) => {
|
||||
original_copy.modified_amts.insert(key, amt);
|
||||
}
|
||||
Message::SetUserData(user_data) => {
|
||||
original_copy.auth = Some(user_data);
|
||||
}
|
||||
Message::UnsetUserData => {
|
||||
original_copy.auth = None;
|
||||
}
|
||||
}
|
||||
original_copy
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_state_handler<'ctx>(
|
||||
cx: Scope<'ctx>,
|
||||
initial: AppState,
|
||||
) -> &'ctx Handler<'ctx, StateMachine, AppState, Message> {
|
||||
Handler::new(cx, initial, StateMachine())
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub struct State {
|
||||
pub recipe_counts: RcSignal<BTreeMap<String, RcSignal<usize>>>,
|
||||
@ -32,23 +150,6 @@ pub struct State {
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
recipe_counts: create_rc_signal(BTreeMap::new()),
|
||||
extras: create_rc_signal(Vec::new()),
|
||||
staples: create_rc_signal(None),
|
||||
recipes: create_rc_signal(BTreeMap::new()),
|
||||
category_map: create_rc_signal(BTreeMap::new()),
|
||||
filtered_ingredients: create_rc_signal(BTreeSet::new()),
|
||||
modified_amts: create_rc_signal(BTreeMap::new()),
|
||||
auth: create_rc_signal(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn provide_context(cx: Scope) {
|
||||
provide_context(cx, std::rc::Rc::new(Self::new()));
|
||||
}
|
||||
|
||||
pub fn get_from_context(cx: Scope) -> std::rc::Rc<Self> {
|
||||
use_context::<std::rc::Rc<Self>>(cx).clone()
|
||||
}
|
||||
|
@ -14,17 +14,17 @@
|
||||
|
||||
use sycamore::prelude::*;
|
||||
|
||||
use crate::app_state;
|
||||
use crate::app_state::{AppState, Message, StateMachine};
|
||||
use sycamore_state::Handler;
|
||||
|
||||
#[component]
|
||||
pub fn Header<G: Html>(cx: Scope) -> View<G> {
|
||||
let state = app_state::State::get_from_context(cx);
|
||||
let login = create_memo(cx, move || {
|
||||
let user_id = state.auth.get();
|
||||
match user_id.as_ref() {
|
||||
Some(user_data) => format!("{}", user_data.user_id),
|
||||
pub fn Header<'ctx, G: Html>(
|
||||
cx: Scope<'ctx>,
|
||||
h: &'ctx Handler<'ctx, StateMachine, AppState, Message>,
|
||||
) -> View<G> {
|
||||
let login = h.get_selector(cx, |sig| match &sig.get().auth {
|
||||
Some(id) => id.user_id.clone(),
|
||||
None => "Login".to_owned(),
|
||||
}
|
||||
});
|
||||
view! {cx,
|
||||
nav(class="no-print") {
|
||||
|
@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
use sycamore::{futures::spawn_local_scoped, prelude::*};
|
||||
use tracing::{error, info, instrument};
|
||||
use tracing::{info, instrument};
|
||||
|
||||
use crate::components::{Footer, Header};
|
||||
use crate::{api, routing::Handler as RouteHandler};
|
||||
@ -20,24 +20,20 @@ use crate::{api, routing::Handler as RouteHandler};
|
||||
#[instrument]
|
||||
#[component]
|
||||
pub fn UI<G: Html>(cx: Scope) -> View<G> {
|
||||
crate::app_state::State::provide_context(cx);
|
||||
api::HttpStore::provide_context(cx, "/api".to_owned());
|
||||
info!("Starting UI");
|
||||
|
||||
let app_state = crate::app_state::AppState::new();
|
||||
let handler = crate::app_state::get_state_handler(cx, app_state);
|
||||
let view = create_signal(cx, View::empty());
|
||||
// FIXME(jwall): We need a way to trigger refreshes when required. Turn this
|
||||
// into a create_effect with a refresh signal stored as a context.
|
||||
spawn_local_scoped(cx, {
|
||||
let store = api::HttpStore::get_from_context(cx);
|
||||
let state = crate::app_state::State::get_from_context(cx);
|
||||
async move {
|
||||
if let Err(err) = api::init_page_state(store.as_ref(), state.as_ref()).await {
|
||||
error!(?err);
|
||||
};
|
||||
api::init_app_state(cx, handler).await;
|
||||
// TODO(jwall): This needs to be moved into the RouteHandler
|
||||
view.set(view! { cx,
|
||||
div(class="app") {
|
||||
Header { }
|
||||
Header(handler)
|
||||
RouteHandler()
|
||||
Footer { }
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user