mirror of
https://github.com/zaphar/kitchen.git
synced 2025-07-22 19:40:14 -04:00
Implement Save Recipe functionality
This commit is contained in:
parent
a8407d51ef
commit
3094dee9f7
@ -18,17 +18,7 @@
|
||||
},
|
||||
"query": "select password_hashed from users where id = ?"
|
||||
},
|
||||
"2519fa6cd665764d0c9a0152e5c8ce20152fc4853c5cd9c34259cec27d8bf47e": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
}
|
||||
},
|
||||
"query": "insert into categories (user_id, category_text) values (?, ?)"
|
||||
},
|
||||
"3311b1ceb15128dca0a3554f1e0f50c03e6c531919a6be1e7f9f940544236143": {
|
||||
"3fd4017569dca4fe73e97e0e2bd612027a8c1b17b0b10faabd6459f56ca1c0bb": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
@ -36,7 +26,7 @@
|
||||
"Right": 3
|
||||
}
|
||||
},
|
||||
"query": "insert into recipes (user_id, recipe_id, recipe_text) values (?, ?, ?)"
|
||||
"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"
|
||||
},
|
||||
"5d743897fb0d8fd54c3708f1b1c6e416346201faa9e28823c1ba5a421472b1fa": {
|
||||
"describe": {
|
||||
@ -58,6 +48,16 @@
|
||||
},
|
||||
"query": "delete from sessions where id = ?"
|
||||
},
|
||||
"8490e1bb40879caed62ac1c38cb9af48246f3451b6f7f1e1f33850f1dbe25f58": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
}
|
||||
},
|
||||
"query": "insert into categories (user_id, category_text) values (?, ?)\n on conflict(user_id) do update set category_text=excluded.category_text"
|
||||
},
|
||||
"928a479ca0f765ec7715bf8784c5490e214486edbf5b78fd501823feb328375b": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
|
@ -137,7 +137,7 @@ async fn api_categories(
|
||||
async fn api_save_categories(
|
||||
Extension(app_store): Extension<Arc<storage::SqliteStore>>,
|
||||
session: storage::UserIdFromSession,
|
||||
categories: String,
|
||||
Json(categories): Json<String>,
|
||||
) -> impl IntoResponse {
|
||||
use storage::{UserId, UserIdFromSession::FoundUserId};
|
||||
if let FoundUserId(UserId(id)) = session {
|
||||
@ -159,12 +159,12 @@ async fn api_save_categories(
|
||||
async fn api_save_recipe(
|
||||
Extension(app_store): Extension<Arc<storage::SqliteStore>>,
|
||||
session: storage::UserIdFromSession,
|
||||
Json(recipe): Json<RecipeEntry>,
|
||||
Json(recipes): Json<Vec<RecipeEntry>>,
|
||||
) -> impl IntoResponse {
|
||||
use storage::{UserId, UserIdFromSession::FoundUserId};
|
||||
if let FoundUserId(UserId(id)) = session {
|
||||
if let Err(e) = app_store
|
||||
.store_recipes_for_user(id.as_str(), &vec![recipe])
|
||||
.store_recipes_for_user(id.as_str(), &recipes)
|
||||
.await
|
||||
{
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, format!("{:?}", e));
|
||||
|
@ -307,7 +307,8 @@ impl APIStore for SqliteStore {
|
||||
let recipe_id = entry.recipe_id().to_owned();
|
||||
let recipe_text = entry.recipe_text().to_owned();
|
||||
sqlx::query!(
|
||||
"insert into recipes (user_id, recipe_id, recipe_text) values (?, ?, ?)",
|
||||
"insert into recipes (user_id, recipe_id, recipe_text) values (?, ?, ?)
|
||||
on conflict(user_id, recipe_id) do update set recipe_text=excluded.recipe_text",
|
||||
user_id,
|
||||
recipe_id,
|
||||
recipe_text,
|
||||
@ -320,7 +321,8 @@ impl APIStore for SqliteStore {
|
||||
|
||||
async fn store_categories_for_user(&self, user_id: &str, categories: &str) -> Result<()> {
|
||||
sqlx::query!(
|
||||
"insert into categories (user_id, category_text) values (?, ?)",
|
||||
"insert into categories (user_id, category_text) values (?, ?)
|
||||
on conflict(user_id) do update set category_text=excluded.category_text",
|
||||
user_id,
|
||||
categories,
|
||||
)
|
||||
|
@ -19,8 +19,6 @@ use async_std::{
|
||||
stream::StreamExt,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use reqwasm;
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use tracing::warn;
|
||||
@ -47,13 +45,6 @@ impl From<std::string::FromUtf8Error> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
impl From<reqwasm::Error> for Error {
|
||||
fn from(item: reqwasm::Error) -> Self {
|
||||
Error(format!("{:?}", item))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait TenantStoreFactory<S>
|
||||
where
|
||||
S: RecipeStore,
|
||||
@ -157,50 +148,3 @@ impl RecipeStore for AsyncFileStore {
|
||||
Ok(Some(entry_vec))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct HttpStore {
|
||||
root: String,
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
impl HttpStore {
|
||||
pub fn new(root: String) -> Self {
|
||||
Self { root }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[async_trait(?Send)]
|
||||
impl RecipeStore for HttpStore {
|
||||
#[instrument]
|
||||
async fn get_categories(&self) -> Result<Option<String>, Error> {
|
||||
let mut path = self.root.clone();
|
||||
path.push_str("/categories");
|
||||
let resp = reqwasm::http::Request::get(&path).send().await?;
|
||||
if resp.status() == 404 {
|
||||
debug!("Categories returned 404");
|
||||
Ok(None)
|
||||
} else if resp.status() != 200 {
|
||||
Err(format!("Status: {}", resp.status()).into())
|
||||
} else {
|
||||
debug!("We got a valid response back!");
|
||||
let resp = resp.text().await;
|
||||
Ok(Some(resp.map_err(|e| format!("{}", e))?))
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn get_recipes(&self) -> Result<Option<Vec<RecipeEntry>>, Error> {
|
||||
let mut path = self.root.clone();
|
||||
path.push_str("/recipes");
|
||||
let resp = reqwasm::http::Request::get(&path).send().await?;
|
||||
if resp.status() != 200 {
|
||||
Err(format!("Status: {}", resp.status()).into())
|
||||
} else {
|
||||
debug!("We got a valid response back!");
|
||||
Ok(resp.json().await.map_err(|e| format!("{}", e))?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
use recipe_store::RecipeEntry;
|
||||
// Copyright 2022 Jeremy Wall
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@ -12,11 +11,12 @@ use recipe_store::RecipeEntry;
|
||||
// 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::prelude::*;
|
||||
use tracing::error;
|
||||
use sycamore::{futures::spawn_local_in_scope, prelude::*};
|
||||
use tracing::{debug, error};
|
||||
use web_sys::HtmlDialogElement;
|
||||
|
||||
use crate::{js_lib::get_element_by_id, service::get_appservice_from_context};
|
||||
use recipe_store::RecipeEntry;
|
||||
use recipes;
|
||||
|
||||
fn get_error_dialog() -> HtmlDialogElement {
|
||||
@ -42,8 +42,27 @@ fn check_recipe_parses(text: &str, error_text: Signal<String>) -> bool {
|
||||
|
||||
#[component(Editor<G>)]
|
||||
fn editor(recipe: RecipeEntry) -> View<G> {
|
||||
let id = Signal::new(recipe.recipe_id().to_owned());
|
||||
let text = Signal::new(recipe.recipe_text().to_owned());
|
||||
let error_text = Signal::new(String::new());
|
||||
let app_service = get_appservice_from_context();
|
||||
let save_signal = Signal::new(());
|
||||
|
||||
create_effect(
|
||||
cloned!((id, app_service, text, save_signal, error_text) => move || {
|
||||
save_signal.get();
|
||||
spawn_local_in_scope({
|
||||
cloned!((id, app_service, text, error_text) => async move {
|
||||
if let Err(e) = app_service
|
||||
.save_recipes(vec![RecipeEntry(id.get_untracked().as_ref().clone(), text.get_untracked().as_ref().clone())])
|
||||
.await {
|
||||
error!(?e, "Failed to save recipe");
|
||||
error_text.set(format!("{:?}", e));
|
||||
};
|
||||
})
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
let dialog_view = cloned!((error_text) => view! {
|
||||
dialog(id="error-dialog") {
|
||||
@ -72,7 +91,8 @@ fn editor(recipe: RecipeEntry) -> View<G> {
|
||||
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?
|
||||
debug!("triggering a save");
|
||||
save_signal.trigger_subscribers();
|
||||
};
|
||||
})) { "Save" }
|
||||
})
|
||||
|
@ -13,43 +13,33 @@
|
||||
// limitations under the License.
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use reqwasm;
|
||||
//use serde::{Deserialize, Serialize};
|
||||
use serde_json::{from_str, to_string};
|
||||
use sycamore::{context::use_context, prelude::*};
|
||||
use tracing::{debug, error, info, instrument, warn};
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::{window, Element, Storage};
|
||||
use web_sys::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>>()
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn get_appservice_from_context() -> AppService<HttpStore> {
|
||||
use_context::<AppService<HttpStore>>()
|
||||
pub fn get_appservice_from_context() -> AppService {
|
||||
use_context::<AppService>()
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AppService<S>
|
||||
where
|
||||
S: RecipeStore,
|
||||
{
|
||||
pub struct AppService {
|
||||
recipes: Signal<BTreeMap<String, Signal<Recipe>>>,
|
||||
staples: Signal<Option<Recipe>>,
|
||||
category_map: Signal<BTreeMap<String, String>>,
|
||||
menu_list: Signal<BTreeMap<String, usize>>,
|
||||
store: S,
|
||||
store: HttpStore,
|
||||
}
|
||||
|
||||
impl<S> AppService<S>
|
||||
where
|
||||
S: RecipeStore,
|
||||
{
|
||||
pub fn new(store: S) -> Self {
|
||||
impl AppService {
|
||||
pub fn new(store: HttpStore) -> Self {
|
||||
Self {
|
||||
recipes: Signal::new(BTreeMap::new()),
|
||||
staples: Signal::new(None),
|
||||
@ -262,6 +252,16 @@ where
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn save_recipes(&self, recipes: Vec<RecipeEntry>) -> Result<(), String> {
|
||||
self.store.save_recipes(recipes).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn save_categories(&self, categories: String) -> Result<(), String> {
|
||||
self.store.save_categories(categories).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_recipes(&mut self, recipes: BTreeMap<String, Recipe>) {
|
||||
self.recipes.set(
|
||||
recipes
|
||||
@ -275,3 +275,111 @@ where
|
||||
self.category_map.set(categories);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Error(String);
|
||||
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(item: std::io::Error) -> Self {
|
||||
Error(format!("{:?}", item))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for String {
|
||||
fn from(item: Error) -> Self {
|
||||
format!("{:?}", item)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Error {
|
||||
fn from(item: String) -> Self {
|
||||
Error(item)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::string::FromUtf8Error> for Error {
|
||||
fn from(item: std::string::FromUtf8Error) -> Self {
|
||||
Error(format!("{:?}", item))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<reqwasm::Error> for Error {
|
||||
fn from(item: reqwasm::Error) -> Self {
|
||||
Error(format!("{:?}", item))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct HttpStore {
|
||||
root: String,
|
||||
}
|
||||
|
||||
impl HttpStore {
|
||||
pub fn new(root: String) -> Self {
|
||||
Self { root }
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn get_categories(&self) -> Result<Option<String>, Error> {
|
||||
let mut path = self.root.clone();
|
||||
path.push_str("/categories");
|
||||
let resp = reqwasm::http::Request::get(&path).send().await?;
|
||||
if resp.status() == 404 {
|
||||
debug!("Categories returned 404");
|
||||
Ok(None)
|
||||
} else if resp.status() != 200 {
|
||||
Err(format!("Status: {}", resp.status()).into())
|
||||
} else {
|
||||
debug!("We got a valid response back!");
|
||||
let resp = resp.text().await;
|
||||
Ok(Some(resp.map_err(|e| format!("{}", e))?))
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn get_recipes(&self) -> Result<Option<Vec<RecipeEntry>>, Error> {
|
||||
let mut path = self.root.clone();
|
||||
path.push_str("/recipes");
|
||||
let resp = reqwasm::http::Request::get(&path).send().await?;
|
||||
if resp.status() != 200 {
|
||||
Err(format!("Status: {}", resp.status()).into())
|
||||
} else {
|
||||
debug!("We got a valid response back!");
|
||||
Ok(resp.json().await.map_err(|e| format!("{}", e))?)
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(recipes), fields(count=recipes.len()))]
|
||||
async fn save_recipes(&self, recipes: Vec<RecipeEntry>) -> Result<(), Error> {
|
||||
let mut path = self.root.clone();
|
||||
path.push_str("/recipes");
|
||||
let resp = reqwasm::http::Request::post(&path)
|
||||
.body(to_string(&recipes).expect("Unable to serialize recipe entries"))
|
||||
.header("content-type", "application/json")
|
||||
.send()
|
||||
.await?;
|
||||
if resp.status() != 200 {
|
||||
Err(format!("Status: {}", resp.status()).into())
|
||||
} else {
|
||||
debug!("We got a valid response back!");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(categories))]
|
||||
async fn save_categories(&self, categories: String) -> Result<(), Error> {
|
||||
let mut path = self.root.clone();
|
||||
path.push_str("/recipes");
|
||||
let resp = reqwasm::http::Request::post(&path)
|
||||
.body(to_string(&categories).expect("Unable to encode categories as json"))
|
||||
.header("content-type", "application/json")
|
||||
.send()
|
||||
.await?;
|
||||
if resp.status() != 200 {
|
||||
Err(format!("Status: {}", resp.status()).into())
|
||||
} else {
|
||||
debug!("We got a valid response back!");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,10 +12,14 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
use crate::pages::*;
|
||||
use crate::{app_state::*, components::*, router_integration::*, service::AppService};
|
||||
use crate::{
|
||||
app_state::*,
|
||||
components::*,
|
||||
router_integration::*,
|
||||
service::{self, AppService},
|
||||
};
|
||||
use tracing::{debug, error, info, instrument};
|
||||
|
||||
use recipe_store::{self, *};
|
||||
use sycamore::{
|
||||
context::{ContextProvider, ContextProviderProps},
|
||||
futures::spawn_local_in_scope,
|
||||
@ -56,13 +60,8 @@ fn route_switch<G: Html>(route: ReadSignal<AppRoutes>) -> View<G> {
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn get_appservice() -> AppService<AsyncFileStore> {
|
||||
AppService::new(recipe_store::AsyncFileStore::new("/".to_owned()))
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn get_appservice() -> AppService<HttpStore> {
|
||||
AppService::new(recipe_store::HttpStore::new("/api/v1".to_owned()))
|
||||
fn get_appservice() -> AppService {
|
||||
AppService::new(service::HttpStore::new("/api/v1".to_owned()))
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
|
Loading…
x
Reference in New Issue
Block a user