Add Edit link for recipes

This commit is contained in:
Jeremy Wall 2022-09-05 17:49:00 -04:00
parent 9e66d6e66f
commit a8407d51ef
6 changed files with 138 additions and 11 deletions

View File

@ -38,6 +38,7 @@ features = [
"History",
"HtmlAnchorElement",
"HtmlBaseElement",
"HtmlDialogElement",
"KeyboardEvent",
"Location",
"PopStateEvent",

View File

@ -19,14 +19,14 @@
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" name="viewport"
content="width=device-width, initial-scale=1.0" charset="UTF-8">
<link rel="stylesheet" href="static/pico.min.css">
<link rel="stylesheet" href="static/app.css">
<link rel="stylesheet" href="/ui/static/pico.min.css">
<link rel="stylesheet" href="/ui/static/app.css">
</head>
<body>
<div id="main"></div>
<script type="module">
import init, { } from './kitchen_wasm.js';
import init, { } from '/ui/kitchen_wasm.js';
async function run() {
await init();

View File

@ -1,3 +1,4 @@
use recipe_store::RecipeEntry;
// Copyright 2022 Jeremy Wall
//
// Licensed under the Apache License, Version 2.0 (the "License");
@ -11,10 +12,71 @@
// 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 recipes;
use sycamore::prelude::*;
use tracing::error;
use web_sys::HtmlDialogElement;
use crate::service::get_appservice_from_context;
use crate::{js_lib::get_element_by_id, service::get_appservice_from_context};
use recipes;
fn get_error_dialog() -> HtmlDialogElement {
get_element_by_id::<HtmlDialogElement>("error-dialog")
.expect("error-dialog isn't an html dialog element!")
.unwrap()
}
fn check_recipe_parses(text: &str, error_text: Signal<String>) -> bool {
if let Err(e) = recipes::parse::as_recipe(text) {
error!(?e, "Error parsing recipe");
error_text.set(e);
let el = get_error_dialog();
el.show();
false
} else {
error_text.set(String::new());
let el = get_error_dialog();
el.close();
true
}
}
#[component(Editor<G>)]
fn editor(recipe: RecipeEntry) -> View<G> {
let text = Signal::new(recipe.recipe_text().to_owned());
let error_text = Signal::new(String::new());
let dialog_view = cloned!((error_text) => view! {
dialog(id="error-dialog") {
article{
header {
a(href="#", on:click=|_| {
let el = get_error_dialog();
el.close();
}, class="close")
"Invalid Recipe"
}
p {
(error_text.get().clone())
}
}
}
});
cloned!((text, error_text) => view! {
(dialog_view)
textarea(bind:value=text.clone(), rows=20)
a(role="button" , href="#", on:click=cloned!((text, error_text) => move |_| {
let unparsed = text.get();
check_recipe_parses(unparsed.as_str(), error_text.clone());
})) { "Check" } " "
a(role="button", href="#", on:click=cloned!((text, error_text) => move |_| {
let unparsed = text.get();
if check_recipe_parses(unparsed.as_str(), error_text.clone()) {
// TODO(jwall): Now actually save the recipe?
};
})) { "Save" }
})
}
#[component(Steps<G>)]
fn steps(steps: ReadSignal<Vec<recipes::Step>>) -> View<G> {
@ -50,7 +112,11 @@ fn steps(steps: ReadSignal<Vec<recipes::Step>>) -> View<G> {
pub fn recipe(idx: ReadSignal<String>) -> View<G> {
let app_service = get_appservice_from_context();
let view = Signal::new(View::empty());
create_effect(cloned!((app_service, view) => move || {
let show_edit = Signal::new(false);
create_effect(cloned!((idx, app_service, view, show_edit) => move || {
if *show_edit.get() {
return;
}
let recipe_id: String = idx.get().as_ref().to_owned();
if let Some(recipe) = app_service.get_recipes().get().get(&recipe_id) {
let recipe = recipe.clone();
@ -61,6 +127,7 @@ pub fn recipe(idx: ReadSignal<String>) -> View<G> {
let steps = create_memo(cloned!((recipe) => move || recipe.get().steps.clone()));
view.set(view! {
div(class="recipe") {
h1(class="recipe_title") { (title.get()) }
div(class="recipe_description") {
(desc.get())
}
@ -69,7 +136,20 @@ pub fn recipe(idx: ReadSignal<String>) -> View<G> {
});
}
}));
create_effect(cloned!((idx, app_service, view, show_edit) => move || {
let recipe_id: String = idx.get().as_ref().to_owned();
if !(*show_edit.get()) {
return;
}
if let Some(entry) = app_service.fetch_recipe_text(recipe_id.as_str()).expect("No such recipe") {
view.set(view! {
Editor(entry)
});
}
}));
view! {
a(role="button", href="#", on:click=cloned!((show_edit) => move |_| { show_edit.set(true); })) { "Edit" } " "
a(role="button", href="#", on:click=cloned!((show_edit) => move |_| { show_edit.set(false); })) { "View" }
(view.get().as_ref())
}
}

29
web/src/js_lib.rs Normal file
View File

@ -0,0 +1,29 @@
// Copyright 2022 Jeremy Wall (Jeremy@marzhilsltudios.com)
//
// 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 wasm_bindgen::{JsCast, JsValue};
use web_sys::{window, Element, Storage};
pub fn get_element_by_id<E>(id: &str) -> Result<Option<E>, Element>
where
E: JsCast,
{
match window().unwrap().document().unwrap().get_element_by_id(id) {
Some(e) => e.dyn_into::<E>().map(|e| Some(e)),
None => Ok(None),
}
}
pub fn get_storage() -> Result<Option<Storage>, JsValue> {
window().unwrap().local_storage()
}

View File

@ -13,6 +13,7 @@
// limitations under the License.
mod app_state;
mod components;
mod js_lib;
mod pages;
mod router_integration;
mod service;

View File

@ -16,11 +16,14 @@ use std::collections::{BTreeMap, BTreeSet};
use serde_json::{from_str, to_string};
use sycamore::{context::use_context, prelude::*};
use tracing::{debug, error, info, instrument, warn};
use web_sys::{window, Storage};
use wasm_bindgen::JsCast;
use web_sys::{window, Element, Storage};
use recipe_store::*;
use recipes::{parse, Ingredient, IngredientAccumulator, Recipe};
use crate::js_lib;
#[cfg(not(target_arch = "wasm32"))]
pub fn get_appservice_from_context() -> AppService<AsyncFileStore> {
use_context::<AppService<AsyncFileStore>>()
@ -57,10 +60,7 @@ where
}
fn get_storage(&self) -> Result<Option<Storage>, String> {
window()
.unwrap()
.local_storage()
.map_err(|e| format!("{:?}", e))
js_lib::get_storage().map_err(|e| format!("{:?}", e))
}
#[instrument(skip(self))]
@ -156,6 +156,22 @@ where
}
}
pub fn fetch_recipe_text(&self, id: &str) -> Result<Option<RecipeEntry>, String> {
let storage = self.get_storage()?.unwrap();
if let Some(s) = storage
.get_item("recipes")
.map_err(|e| format!("{:?}", e))?
{
let parsed = from_str::<Vec<RecipeEntry>>(&s).map_err(|e| format!("{}", e))?;
for r in parsed {
if r.recipe_id() == id {
return Ok(Some(r));
}
}
}
return Ok(None);
}
async fn fetch_recipes(
&self,
) -> Result<(Option<Recipe>, Option<BTreeMap<String, Recipe>>), String> {