Recipe can now be scheduled
This commit is contained in:
parent
ae6da1a5ae
commit
fbef990022
18 changed files with 233 additions and 51 deletions
7
Cargo.lock
generated
7
Cargo.lock
generated
|
|
@ -356,6 +356,7 @@ dependencies = [
|
||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
|
"pure-rust-locales",
|
||||||
"serde",
|
"serde",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
|
|
@ -1853,6 +1854,12 @@ dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pure-rust-locales"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1190fd18ae6ce9e137184f207593877e70f39b015040156b1e05081cdfe3733a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.38"
|
version = "1.0.38"
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
|
|
||||||
#toast.show {
|
#toast.show {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
animation: fadein 0.5s, fadeout 0.5s 3.6s;
|
animation: fadein 0.5s, fadeout 0.5s 9.6s;
|
||||||
animation-iteration-count: 1;
|
animation-iteration-count: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
-- Datetimes are stored as 'ISO 8601' text format.
|
||||||
|
-- For example: '2025-01-07T10:41:05.697884837+00:00'.
|
||||||
|
|
||||||
-- Version 1 is the initial structure.
|
-- Version 1 is the initial structure.
|
||||||
CREATE TABLE [Version] (
|
CREATE TABLE [Version] (
|
||||||
[id] INTEGER PRIMARY KEY,
|
[id] INTEGER PRIMARY KEY,
|
||||||
|
|
@ -165,9 +168,11 @@ CREATE TABLE [RecipeScheduled] (
|
||||||
[id] INTEGER PRIMARY KEY,
|
[id] INTEGER PRIMARY KEY,
|
||||||
[user_id] INTEGER NOT NULL,
|
[user_id] INTEGER NOT NULL,
|
||||||
[recipe_id] INTEGER NOT NULL,
|
[recipe_id] INTEGER NOT NULL,
|
||||||
[date] TEXT NOT NULL,
|
[date] TEXT NOT NULL, -- In form of 'YYYY-MM-DD'.
|
||||||
[servings] INTEGER, -- If NULL use [recipe].[servings].
|
[servings] INTEGER, -- If NULL use [recipe].[servings].
|
||||||
|
|
||||||
|
UNIQUE([user_id], [recipe_id], [date]),
|
||||||
|
|
||||||
FOREIGN KEY([user_id]) REFERENCES [User]([id]) ON DELETE CASCADE,
|
FOREIGN KEY([user_id]) REFERENCES [User]([id]) ON DELETE CASCADE,
|
||||||
FOREIGN KEY([recipe_id]) REFERENCES [Recipe]([id]) ON DELETE CASCADE
|
FOREIGN KEY([recipe_id]) REFERENCES [Recipe]([id]) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -752,7 +752,7 @@ VALUES ($1, $2)
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn add_schedule_recipe(
|
pub async fn add_scheduled_recipe(
|
||||||
&self,
|
&self,
|
||||||
user_id: i64,
|
user_id: i64,
|
||||||
recipe_id: i64,
|
recipe_id: i64,
|
||||||
|
|
@ -775,7 +775,7 @@ VALUES ($1, $2, $3, $4)
|
||||||
.map_err(DBError::from)
|
.map_err(DBError::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn remove_scheduled_recipe(
|
pub async fn rm_scheduled_recipe(
|
||||||
&self,
|
&self,
|
||||||
user_id: i64,
|
user_id: i64,
|
||||||
recipe_id: i64,
|
recipe_id: i64,
|
||||||
|
|
@ -964,13 +964,13 @@ VALUES
|
||||||
let tomorrow = today + Days::new(1);
|
let tomorrow = today + Days::new(1);
|
||||||
|
|
||||||
connection
|
connection
|
||||||
.add_schedule_recipe(user_id, recipe_id_1, today, 4)
|
.add_scheduled_recipe(user_id, recipe_id_1, today, 4)
|
||||||
.await?;
|
.await?;
|
||||||
connection
|
connection
|
||||||
.add_schedule_recipe(user_id, recipe_id_2, yesterday, 4)
|
.add_scheduled_recipe(user_id, recipe_id_2, yesterday, 4)
|
||||||
.await?;
|
.await?;
|
||||||
connection
|
connection
|
||||||
.add_schedule_recipe(user_id, recipe_id_1, tomorrow, 4)
|
.add_scheduled_recipe(user_id, recipe_id_1, tomorrow, 4)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
@ -1008,13 +1008,13 @@ VALUES
|
||||||
);
|
);
|
||||||
|
|
||||||
connection
|
connection
|
||||||
.remove_scheduled_recipe(user_id, recipe_id_1, today)
|
.rm_scheduled_recipe(user_id, recipe_id_1, today)
|
||||||
.await?;
|
.await?;
|
||||||
connection
|
connection
|
||||||
.remove_scheduled_recipe(user_id, recipe_id_2, yesterday)
|
.rm_scheduled_recipe(user_id, recipe_id_2, yesterday)
|
||||||
.await?;
|
.await?;
|
||||||
connection
|
connection
|
||||||
.remove_scheduled_recipe(user_id, recipe_id_1, tomorrow)
|
.rm_scheduled_recipe(user_id, recipe_id_1, tomorrow)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
|
||||||
|
|
@ -181,6 +181,14 @@ async fn main() {
|
||||||
"/calendar/get_scheduled_recipes",
|
"/calendar/get_scheduled_recipes",
|
||||||
get(services::ron::get_scheduled_recipes),
|
get(services::ron::get_scheduled_recipes),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/calendar/schedule_recipe",
|
||||||
|
post(services::ron::schedule_recipe),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/calendar/remove_scheduled_recipe",
|
||||||
|
delete(services::ron::rm_scheduled_recipe),
|
||||||
|
)
|
||||||
.fallback(services::ron::not_found);
|
.fallback(services::ron::not_found);
|
||||||
|
|
||||||
let fragments_routes = Router::new().route(
|
let fragments_routes = Router::new().route(
|
||||||
|
|
|
||||||
|
|
@ -631,6 +631,36 @@ pub async fn get_scheduled_recipes(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[debug_handler]
|
||||||
|
pub async fn schedule_recipe(
|
||||||
|
State(connection): State<db::Connection>,
|
||||||
|
Extension(user): Extension<Option<model::User>>,
|
||||||
|
ExtractRon(ron): ExtractRon<common::ron_api::ScheduleRecipe>,
|
||||||
|
) -> Result<impl IntoResponse> {
|
||||||
|
check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
|
||||||
|
if let Some(user) = user {
|
||||||
|
connection
|
||||||
|
.add_scheduled_recipe(user.id, ron.recipe_id, ron.date, ron.servings)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Ok(StatusCode::OK)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[debug_handler]
|
||||||
|
pub async fn rm_scheduled_recipe(
|
||||||
|
State(connection): State<db::Connection>,
|
||||||
|
Extension(user): Extension<Option<model::User>>,
|
||||||
|
ExtractRon(ron): ExtractRon<common::ron_api::ScheduledRecipe>,
|
||||||
|
) -> Result<impl IntoResponse> {
|
||||||
|
check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
|
||||||
|
if let Some(user) = user {
|
||||||
|
connection
|
||||||
|
.rm_scheduled_recipe(user.id, ron.recipe_id, ron.date)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Ok(StatusCode::OK)
|
||||||
|
}
|
||||||
|
|
||||||
/// 404 ///
|
/// 404 ///
|
||||||
|
|
||||||
#[debug_handler]
|
#[debug_handler]
|
||||||
|
|
|
||||||
|
|
@ -141,6 +141,9 @@ pub enum Sentence {
|
||||||
CalendarOctober,
|
CalendarOctober,
|
||||||
CalendarNovember,
|
CalendarNovember,
|
||||||
CalendarDecember,
|
CalendarDecember,
|
||||||
|
CalendarAddToPlanner,
|
||||||
|
CalendarAddToPlannerSuccess,
|
||||||
|
CalendarDateFormat, // See https://docs.rs/chrono/latest/chrono/format/strftime/index.html.
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const DEFAULT_LANGUAGE_CODE: &str = "en";
|
pub const DEFAULT_LANGUAGE_CODE: &str = "en";
|
||||||
|
|
@ -181,6 +184,10 @@ impl Tr {
|
||||||
pub fn current_lang_code(&self) -> &str {
|
pub fn current_lang_code(&self) -> &str {
|
||||||
&self.lang.code
|
&self.lang.code
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn current_lang_and_territory_code(&self) -> String {
|
||||||
|
format!("{}-{}", self.lang.code, self.lang.territory)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// #[macro_export]
|
// #[macro_export]
|
||||||
|
|
@ -200,6 +207,7 @@ impl Tr {
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct StoredLanguage {
|
struct StoredLanguage {
|
||||||
code: String,
|
code: String,
|
||||||
|
territory: String,
|
||||||
name: String,
|
name: String,
|
||||||
translation: Vec<(Sentence, String)>,
|
translation: Vec<(Sentence, String)>,
|
||||||
}
|
}
|
||||||
|
|
@ -207,6 +215,7 @@ struct StoredLanguage {
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct Language {
|
struct Language {
|
||||||
code: String,
|
code: String,
|
||||||
|
territory: String,
|
||||||
name: String,
|
name: String,
|
||||||
translation: Vec<String>,
|
translation: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
@ -215,6 +224,7 @@ impl Language {
|
||||||
pub fn from_stored_language(stored_language: StoredLanguage) -> Self {
|
pub fn from_stored_language(stored_language: StoredLanguage) -> Self {
|
||||||
Self {
|
Self {
|
||||||
code: stored_language.code,
|
code: stored_language.code,
|
||||||
|
territory: stored_language.territory,
|
||||||
name: stored_language.name,
|
name: stored_language.name,
|
||||||
translation: {
|
translation: {
|
||||||
let mut translation = vec![String::new(); Sentence::COUNT];
|
let mut translation = vec![String::new(); Sentence::COUNT];
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="{{ tr.current_lang_and_territory_code() }}">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
{% if crate::data::model::can_user_edit_recipe(user, recipe) %}
|
{% if crate::data::model::can_user_edit_recipe(user, recipe) %}
|
||||||
<a class="edit-recipe" href="/recipe/edit/{{ recipe.id }}" >Edit</a>
|
<a class="edit-recipe" href="/recipe/edit/{{ recipe.id }}" >Edit</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<span class="add-to-planner">Add to planner</span>
|
<span class="add-to-planner">{{ tr.t(Sentence::CalendarAddToPlanner) }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="tags">
|
<div class="tags">
|
||||||
|
|
@ -92,6 +92,9 @@
|
||||||
value="{{ user.default_servings }}"/>
|
value="{{ user.default_servings }}"/>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<span class="calendar-add-to-planner-success">{{ tr.t(Sentence::CalendarAddToPlannerSuccess) }}</span>
|
||||||
|
<span class="calendar-date-format">{{ tr.t(Sentence::CalendarDateFormat) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
code: "en",
|
code: "en",
|
||||||
|
territory: "US",
|
||||||
name: "English",
|
name: "English",
|
||||||
translation: [
|
translation: [
|
||||||
(MainTitle, "Cooking Recipes"),
|
(MainTitle, "Cooking Recipes"),
|
||||||
|
|
@ -124,10 +125,14 @@
|
||||||
(CalendarOctober, "October"),
|
(CalendarOctober, "October"),
|
||||||
(CalendarNovember, "November"),
|
(CalendarNovember, "November"),
|
||||||
(CalendarDecember, "December"),
|
(CalendarDecember, "December"),
|
||||||
|
(CalendarAddToPlanner, "Add to planner"),
|
||||||
|
(CalendarAddToPlannerSuccess, "Recipe {} has been scheduled for {}"),
|
||||||
|
(CalendarDateFormat, "%A, %-d %B, %C%y"),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
code: "fr",
|
code: "fr",
|
||||||
|
territory: "FR",
|
||||||
name: "Français",
|
name: "Français",
|
||||||
translation: [
|
translation: [
|
||||||
(MainTitle, "Recettes de Cuisine"),
|
(MainTitle, "Recettes de Cuisine"),
|
||||||
|
|
@ -251,6 +256,9 @@
|
||||||
(CalendarOctober, "Octobre"),
|
(CalendarOctober, "Octobre"),
|
||||||
(CalendarNovember, "Novembre"),
|
(CalendarNovember, "Novembre"),
|
||||||
(CalendarDecember, "Décembre"),
|
(CalendarDecember, "Décembre"),
|
||||||
|
(CalendarAddToPlanner, "Ajouter au planificateur"),
|
||||||
|
(CalendarAddToPlannerSuccess, "La recette {} a été agendée pour le {}"),
|
||||||
|
(CalendarDateFormat, "%A %-d %B %C%y"),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
@ -17,7 +17,7 @@ pub struct Id {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RECIPE ///
|
/*** RECIPE ***/
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub struct SetRecipeTitle {
|
pub struct SetRecipeTitle {
|
||||||
|
|
@ -159,7 +159,7 @@ pub struct Ingredient {
|
||||||
pub quantity_unit: String,
|
pub quantity_unit: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// PROFILE ///
|
/*** PROFILE ***/
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub struct UpdateProfile {
|
pub struct UpdateProfile {
|
||||||
|
|
@ -168,6 +168,29 @@ pub struct UpdateProfile {
|
||||||
pub password: Option<String>,
|
pub password: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*** Calendar ***/
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct ScheduledRecipes {
|
||||||
|
// (Scheduled date, recipe title, recipe id).
|
||||||
|
pub recipes: Vec<(NaiveDate, String, i64)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct ScheduleRecipe {
|
||||||
|
pub recipe_id: i64,
|
||||||
|
pub date: NaiveDate,
|
||||||
|
pub servings: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct ScheduledRecipe {
|
||||||
|
pub recipe_id: i64,
|
||||||
|
pub date: NaiveDate,
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** Misc ***/
|
||||||
|
|
||||||
pub fn to_string<T>(ron: T) -> String
|
pub fn to_string<T>(ron: T) -> String
|
||||||
where
|
where
|
||||||
T: Serialize,
|
T: Serialize,
|
||||||
|
|
@ -175,11 +198,3 @@ where
|
||||||
// TODO: handle'unwrap'.
|
// TODO: handle'unwrap'.
|
||||||
to_string_pretty(&ron, PrettyConfig::new()).unwrap()
|
to_string_pretty(&ron, PrettyConfig::new()).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calendar ///
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
|
||||||
pub struct ScheduledRecipes {
|
|
||||||
// (Scheduled date, recipe title, recipe id).
|
|
||||||
pub recipes: Vec<(NaiveDate, String, i64)>,
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ default = ["console_error_panic_hook"]
|
||||||
[dependencies]
|
[dependencies]
|
||||||
common = { path = "../common" }
|
common = { path = "../common" }
|
||||||
|
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde", "unstable-locales"] }
|
||||||
|
|
||||||
ron = "0.8"
|
ron = "0.8"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ struct CalendarStateInternal {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct CalendarState {
|
pub struct CalendarState {
|
||||||
internal_state: Rc<RefCell<CalendarStateInternal>>,
|
internal_state: Rc<RefCell<CalendarStateInternal>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -56,7 +56,7 @@ impl CalendarState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setup(calendar: Element) {
|
pub fn setup(calendar: Element) -> CalendarState {
|
||||||
let prev: Element = calendar.selector(".prev");
|
let prev: Element = calendar.selector(".prev");
|
||||||
let next: Element = calendar.selector(".next");
|
let next: Element = calendar.selector(".next");
|
||||||
|
|
||||||
|
|
@ -98,6 +98,8 @@ pub fn setup(calendar: Element) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.forget();
|
.forget();
|
||||||
|
|
||||||
|
state
|
||||||
}
|
}
|
||||||
|
|
||||||
const NB_CALENDAR_ROW: u64 = 5;
|
const NB_CALENDAR_ROW: u64 = 5;
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,26 @@ use crate::{
|
||||||
};
|
};
|
||||||
|
|
||||||
pub async fn show(element_selector: &str) -> bool {
|
pub async fn show(element_selector: &str) -> bool {
|
||||||
show_and_initialize(element_selector, async |_| {}).await
|
show_and_initialize(element_selector, async |_| Some(()))
|
||||||
|
.await
|
||||||
|
.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn show_and_initialize<T>(element_selector: &str, initializer: T) -> bool
|
pub async fn show_and_initialize<T, U>(element_selector: &str, initializer: T) -> Option<U>
|
||||||
where
|
where
|
||||||
T: AsyncFn(Element),
|
T: AsyncFn(Element) -> U,
|
||||||
|
{
|
||||||
|
show_and_initialize_with_ok(element_selector, initializer, |_, result| result).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn show_and_initialize_with_ok<T, V, W, U>(
|
||||||
|
element_selector: &str,
|
||||||
|
initializer: T,
|
||||||
|
ok: V,
|
||||||
|
) -> Option<W>
|
||||||
|
where
|
||||||
|
T: AsyncFn(Element) -> U,
|
||||||
|
V: Fn(Element, U) -> W,
|
||||||
{
|
{
|
||||||
let dialog: HtmlDialogElement = by_id("modal-dialog");
|
let dialog: HtmlDialogElement = by_id("modal-dialog");
|
||||||
|
|
||||||
|
|
@ -24,7 +38,7 @@ where
|
||||||
let element: Element = selector_and_clone(element_selector);
|
let element: Element = selector_and_clone(element_selector);
|
||||||
content_element.set_inner_html("");
|
content_element.set_inner_html("");
|
||||||
content_element.append_child(&element).unwrap();
|
content_element.append_child(&element).unwrap();
|
||||||
initializer(element).await;
|
let init_result = initializer(element.clone()).await;
|
||||||
|
|
||||||
dialog.show_modal().unwrap();
|
dialog.show_modal().unwrap();
|
||||||
|
|
||||||
|
|
@ -34,8 +48,8 @@ where
|
||||||
pin_mut!(click_ok, click_cancel);
|
pin_mut!(click_ok, click_cancel);
|
||||||
|
|
||||||
let result = select! {
|
let result = select! {
|
||||||
() = click_ok => true,
|
() = click_ok => Some(ok(element, init_result)),
|
||||||
() = click_cancel => false,
|
() = click_cancel => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
dialog.close();
|
dialog.close();
|
||||||
|
|
|
||||||
|
|
@ -19,11 +19,6 @@ use crate::{
|
||||||
utils::{by_id, selector, selector_and_clone, SelectorExt},
|
utils::{by_id, selector, selector_and_clone, SelectorExt},
|
||||||
};
|
};
|
||||||
|
|
||||||
use futures::{
|
|
||||||
future::{FutureExt, Ready},
|
|
||||||
pin_mut, select, Future,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn setup_page(recipe_id: i64) -> Result<(), JsValue> {
|
pub fn setup_page(recipe_id: i64) -> Result<(), JsValue> {
|
||||||
// Title.
|
// Title.
|
||||||
{
|
{
|
||||||
|
|
@ -265,6 +260,7 @@ pub fn setup_page(recipe_id: i64) -> Result<(), JsValue> {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
.is_some()
|
||||||
{
|
{
|
||||||
let body = ron_api::Id { id: recipe_id };
|
let body = ron_api::Id { id: recipe_id };
|
||||||
let _ = request::delete::<(), _>("recipe/remove", body).await;
|
let _ = request::delete::<(), _>("recipe/remove", body).await;
|
||||||
|
|
@ -400,6 +396,7 @@ fn create_group_element(group: &ron_api::Group) -> Element {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
.is_some()
|
||||||
{
|
{
|
||||||
let body = ron_api::Id { id: group_id };
|
let body = ron_api::Id { id: group_id };
|
||||||
let _ = request::delete::<(), _>("recipe/remove_group", body).await;
|
let _ = request::delete::<(), _>("recipe/remove_group", body).await;
|
||||||
|
|
@ -542,6 +539,7 @@ fn create_step_element(group_element: &Element, step: &ron_api::Step) -> Element
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
.is_some()
|
||||||
{
|
{
|
||||||
let body = ron_api::Id { id: step_id };
|
let body = ron_api::Id { id: step_id };
|
||||||
let _ = request::delete::<(), _>("recipe/remove_step", body).await;
|
let _ = request::delete::<(), _>("recipe/remove_step", body).await;
|
||||||
|
|
@ -696,6 +694,7 @@ fn create_ingredient_element(step_element: &Element, ingredient: &ron_api::Ingre
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
.is_some()
|
||||||
{
|
{
|
||||||
let body = ron_api::Id { id: ingredient_id };
|
let body = ron_api::Id { id: ingredient_id };
|
||||||
let _ = request::delete::<(), _>("recipe/remove_ingredient", body).await;
|
let _ = request::delete::<(), _>("recipe/remove_ingredient", body).await;
|
||||||
|
|
@ -717,7 +716,7 @@ async fn reload_recipes_list(current_recipe_id: i64) {
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
toast::show(Level::Info, &format!("Internal server error: {}", error));
|
toast::show_message(Level::Info, &format!("Internal server error: {}", error));
|
||||||
}
|
}
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
let list = document().get_element_by_id("recipes-list").unwrap();
|
let list = document().get_element_by_id("recipes-list").unwrap();
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
use std::future::Future;
|
use std::{cell::RefCell, future::Future, rc::Rc, str::FromStr};
|
||||||
|
|
||||||
use common::ron_api;
|
use chrono::Locale;
|
||||||
|
use common::{ron_api, utils::substitute};
|
||||||
use gloo::{
|
use gloo::{
|
||||||
console::console,
|
console::log,
|
||||||
events::EventListener,
|
events::EventListener,
|
||||||
net::http::Request,
|
net::http::Request,
|
||||||
utils::{document, window},
|
utils::{document, window},
|
||||||
|
|
@ -24,13 +25,59 @@ pub fn setup_page(recipe_id: i64) -> Result<(), JsValue> {
|
||||||
let add_to_planner: Element = selector("#recipe-view .add-to-planner");
|
let add_to_planner: Element = selector("#recipe-view .add-to-planner");
|
||||||
EventListener::new(&add_to_planner, "click", move |_event| {
|
EventListener::new(&add_to_planner, "click", move |_event| {
|
||||||
spawn_local(async move {
|
spawn_local(async move {
|
||||||
modal_dialog::show_and_initialize(
|
if let Some((date, servings)) = modal_dialog::show_and_initialize_with_ok(
|
||||||
"#hidden-templates .date-and-servings",
|
"#hidden-templates .date-and-servings",
|
||||||
async |element| {
|
async |element| calendar::setup(element.selector(".calendar")),
|
||||||
calendar::setup(element.selector(".calendar"));
|
|element, calendar_state| {
|
||||||
|
let servings_element: HtmlInputElement = element.selector("#input-servings");
|
||||||
|
(
|
||||||
|
calendar_state.get_selected_date().date_naive(),
|
||||||
|
servings_element.value_as_number() as u32,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await;
|
.await
|
||||||
|
{
|
||||||
|
if request::post::<(), _>(
|
||||||
|
"calendar/schedule_recipe",
|
||||||
|
ron_api::ScheduleRecipe {
|
||||||
|
recipe_id,
|
||||||
|
date,
|
||||||
|
servings,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
toast::show_element_and_initialize(
|
||||||
|
Level::Success,
|
||||||
|
"#hidden-templates .calendar-add-to-planner-success",
|
||||||
|
|element| {
|
||||||
|
let title =
|
||||||
|
selector::<Element>("#recipe-view .recipe-title").inner_html();
|
||||||
|
let date_format =
|
||||||
|
selector::<Element>("#hidden-templates .calendar-date-format")
|
||||||
|
.inner_html();
|
||||||
|
let locale = {
|
||||||
|
let lang_and_territory = selector::<Element>("html")
|
||||||
|
.get_attribute("lang")
|
||||||
|
.unwrap()
|
||||||
|
.replace("-", "_");
|
||||||
|
Locale::from_str(&lang_and_territory).unwrap_or_default()
|
||||||
|
};
|
||||||
|
|
||||||
|
element.set_inner_html(&substitute(
|
||||||
|
&element.inner_html(),
|
||||||
|
"{}",
|
||||||
|
&[
|
||||||
|
&title,
|
||||||
|
&date.format_localized(&date_format, locale).to_string(),
|
||||||
|
],
|
||||||
|
));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.forget();
|
.forget();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
use common::ron_api;
|
use common::ron_api;
|
||||||
use gloo::net::http::{Request, RequestBuilder};
|
use gloo::{
|
||||||
|
console::log,
|
||||||
|
net::http::{Request, RequestBuilder},
|
||||||
|
};
|
||||||
use serde::{de::DeserializeOwned, Serialize};
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
|
@ -62,19 +65,23 @@ where
|
||||||
{
|
{
|
||||||
match request.send().await {
|
match request.send().await {
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
toast::show(Level::Info, &format!("Internal server error: {}", error));
|
toast::show_message(Level::Info, &format!("Internal server error: {}", error));
|
||||||
Err(Error::Gloo(error))
|
Err(Error::Gloo(error))
|
||||||
}
|
}
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
if !response.ok() {
|
if !response.ok() {
|
||||||
toast::show(
|
toast::show_message(
|
||||||
Level::Info,
|
Level::Info,
|
||||||
&format!("HTTP error: {}", response.status_text()),
|
&format!("HTTP error: {}", response.status_text()),
|
||||||
);
|
);
|
||||||
Err(Error::Http(response.status_text()))
|
Err(Error::Http(response.status_text()))
|
||||||
} else {
|
} else {
|
||||||
// Ok(())
|
let mut r = response.binary().await?;
|
||||||
Ok(ron::de::from_bytes::<T>(&response.binary().await?)?)
|
// An empty response is considered to be an unit value.
|
||||||
|
if r.is_empty() {
|
||||||
|
r = b"()".to_vec();
|
||||||
|
}
|
||||||
|
Ok(ron::de::from_bytes::<T>(&r)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
use gloo::{timers::callback::Timeout, utils::document};
|
use gloo::{timers::callback::Timeout, utils::document};
|
||||||
|
use web_sys::Element;
|
||||||
|
|
||||||
|
use crate::utils::{by_id, selector_and_clone, SelectorExt};
|
||||||
|
|
||||||
pub enum Level {
|
pub enum Level {
|
||||||
Success,
|
Success,
|
||||||
|
|
@ -7,12 +10,36 @@ pub enum Level {
|
||||||
Warning,
|
Warning,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn show(level: Level, message: &str) {
|
const TIME_DISPLAYED: u32 = 10_000; // [ms].
|
||||||
|
|
||||||
|
pub fn show_message(level: Level, message: &str) {
|
||||||
let toast_element = document().get_element_by_id("toast").unwrap();
|
let toast_element = document().get_element_by_id("toast").unwrap();
|
||||||
toast_element.set_inner_html(message);
|
toast_element.set_inner_html(message);
|
||||||
toast_element.set_class_name("show");
|
toast_element.set_class_name("show");
|
||||||
|
|
||||||
Timeout::new(4_000, move || {
|
Timeout::new(TIME_DISPLAYED, move || {
|
||||||
|
toast_element.set_class_name("");
|
||||||
|
})
|
||||||
|
.forget();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show_element(level: Level, selector: &str) {
|
||||||
|
show_element_and_initialize(level, selector, |_| {})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show_element_and_initialize<T>(level: Level, selector: &str, initializer: T)
|
||||||
|
where
|
||||||
|
T: Fn(Element),
|
||||||
|
{
|
||||||
|
let toast_element = document().get_element_by_id("toast").unwrap();
|
||||||
|
|
||||||
|
let element: Element = selector_and_clone(selector);
|
||||||
|
toast_element.set_inner_html("");
|
||||||
|
toast_element.append_child(&element).unwrap();
|
||||||
|
initializer(element.clone());
|
||||||
|
toast_element.set_class_name("show");
|
||||||
|
|
||||||
|
Timeout::new(TIME_DISPLAYED, move || {
|
||||||
toast_element.set_class_name("");
|
toast_element.set_class_name("");
|
||||||
})
|
})
|
||||||
.forget();
|
.forget();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue