Add a calendar to schedule a recipe to a chosen date (WIP)

This commit is contained in:
Greg Burri 2025-01-23 03:01:15 +01:00
parent d9449de02b
commit 9d3f9e9c60
15 changed files with 441 additions and 62 deletions

121
frontend/src/calendar.rs Normal file
View file

@ -0,0 +1,121 @@
use std::{
ops::{AddAssign, SubAssign},
sync::{
atomic::{AtomicI32, AtomicU32, Ordering},
Arc,
},
};
use chrono::{offset::Local, Datelike, Days, NaiveDate, Weekday};
use gloo::{console::log, events::EventListener};
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::spawn_local;
use web_sys::Element;
use crate::utils::{by_id, SelectorExt};
pub fn setup(calendar: &Element) {
let prev: Element = calendar.selector(".prev");
let next: Element = calendar.selector(".next");
let current_month = Arc::new(AtomicU32::new(Local::now().month()));
let current_year = Arc::new(AtomicI32::new(Local::now().year()));
display_month(calendar, Local::now().year(), Local::now().month());
let calendar_clone = calendar.clone();
let current_month_clone = current_month.clone();
let current_year_clone = current_year.clone();
EventListener::new(&prev, "click", move |_event| {
let mut m = current_month_clone.load(Ordering::Relaxed) - 1;
if m == 0 {
current_year_clone.fetch_sub(1, Ordering::Relaxed);
m = 12
}
current_month_clone.store(m, Ordering::Relaxed);
display_month(
&calendar_clone,
current_year_clone.load(Ordering::Relaxed),
m,
);
})
.forget();
let calendar_clone = calendar.clone();
let current_month_clone = current_month.clone();
let current_year_clone = current_year.clone();
EventListener::new(&next, "click", move |_event| {
let mut m = current_month_clone.load(Ordering::Relaxed) + 1;
if m == 13 {
current_year_clone.fetch_add(1, Ordering::Relaxed);
m = 1
}
current_month_clone.store(m, Ordering::Relaxed);
display_month(
&calendar_clone,
current_year_clone.load(Ordering::Relaxed),
m,
);
})
.forget();
// now.weekday()
// console!(now.to_string());
}
// fn translate_month(month: u32) -> &'static str {
// match
// }
fn display_month(calendar: &Element, year: i32, month: u32) {
log!(year, month);
calendar
.selector::<Element>(".year")
.set_inner_html(&year.to_string());
for (i, m) in calendar
.selector_all::<Element>(".month")
.into_iter()
.enumerate()
{
if i as u32 + 1 == month {
m.set_class_name("month current");
} else {
m.set_class_name("month");
}
}
// calendar
// .selector::<Element>(".month")
// .set_inner_html(&month.to_string());
let mut current = NaiveDate::from_ymd_opt(year, month, 1).unwrap();
// let mut day = Local:: ;
while (current - Days::new(1)).month() == month {
current = current - Days::new(1);
}
while current.weekday() != Weekday::Mon {
current = current - Days::new(1);
}
for i in 0..7 {
for j in 0..5 {
let li: Element = by_id(&format!("day-{}{}", i, j));
li.set_inner_html(&current.day().to_string());
if current == Local::now().date_naive() {
li.set_class_name("current-month today");
} else if current.month() == month {
li.set_class_name("current-month");
} else {
li.set_class_name("");
}
current = current + Days::new(1);
}
}
}

View file

@ -1,6 +1,8 @@
mod calendar;
mod modal_dialog;
mod on_click;
mod recipe_edit;
mod recipe_view;
mod request;
mod toast;
mod utils;
@ -20,16 +22,24 @@ pub fn main() -> Result<(), JsValue> {
let location = window().location().pathname()?;
let path: Vec<&str> = location.split('/').skip(1).collect();
if let ["recipe", "edit", id] = path[..] {
let id = id.parse::<i64>().unwrap(); // TODO: remove unwrap.
if let Err(error) = recipe_edit::setup_page(id) {
log!(error);
// if let ["recipe", "edit", id] = path[..] {
match path[..] {
["recipe", "edit", id] => {
let id = id.parse::<i64>().unwrap(); // TODO: remove unwrap.
if let Err(error) = recipe_edit::setup_page(id) {
log!(error);
}
}
// Disable: user editing data are now submitted as classic form data.
// ["user", "edit"] => {
// handles::user_edit(document)?;
// }
["recipe", "view", id] => {
let id = id.parse::<i64>().unwrap(); // TODO: remove unwrap.
if let Err(error) = recipe_view::setup_page(id) {
log!(error);
}
}
_ => (), // Disable: user editing data are now submitted as classic form data.
// ["user", "edit"] => {
// handles::user_edit(document)?;
// }
}
let select_language: HtmlSelectElement = by_id("select-website-language");

View file

@ -1,16 +1,39 @@
use futures::{future::FutureExt, pin_mut, select};
use web_sys::{Element, HtmlDialogElement};
use crate::utils::{by_id, SelectorExt};
use crate::{
on_click,
utils::{by_id, selector_and_clone, SelectorExt},
};
use crate::on_click;
pub enum DialogContent<'a, T>
where
T: Fn(&Element),
{
Text(&'a str),
CloneFromElement(&'a str, T),
}
pub async fn show(message: &str) -> bool {
pub async fn show<T>(content: DialogContent<'_, T>) -> bool
where
T: Fn(&Element),
{
let dialog: HtmlDialogElement = by_id("modal-dialog");
let input_ok: Element = dialog.selector(".ok");
let input_cancel: Element = dialog.selector(".cancel");
dialog.selector::<Element>(".content").set_inner_html(message);
let content_element = dialog.selector::<Element>(".content");
match content {
DialogContent::Text(message) => content_element.set_inner_html(message),
DialogContent::CloneFromElement(element_selector, initilizer) => {
let element: Element = selector_and_clone(element_selector);
content_element.set_inner_html("");
content_element.append_child(&element).unwrap();
initilizer(&element);
}
}
dialog.show_modal().unwrap();

View file

@ -20,22 +20,6 @@ use crate::{
utils::{by_id, selector, selector_and_clone, SelectorExt},
};
async fn reload_recipes_list(current_recipe_id: i64) {
match Request::get("/fragments/recipes_list")
.query([("current_recipe_id", current_recipe_id.to_string())])
.send()
.await
{
Err(error) => {
toast::show(Level::Info, &format!("Internal server error: {}", error));
}
Ok(response) => {
let list = document().get_element_by_id("recipes-list").unwrap();
list.set_outer_html(&response.text().await.unwrap());
}
}
}
pub fn setup_page(recipe_id: i64) -> Result<(), JsValue> {
// Title.
{
@ -266,10 +250,10 @@ pub fn setup_page(recipe_id: i64) -> Result<(), JsValue> {
EventListener::new(&delete_button, "click", move |_event| {
let title: HtmlInputElement = by_id("input-title");
spawn_local(async move {
if modal_dialog::show(&format!(
if modal_dialog::show(modal_dialog::DialogContent::<fn(&Element)>::Text(&format!(
"Are you sure to delete the recipe '{}'",
title.value()
))
)))
.await
{
let body = ron_api::Id { id: recipe_id };
@ -314,7 +298,7 @@ pub fn setup_page(recipe_id: i64) -> Result<(), JsValue> {
// Add a new group.
{
let button_add_group: HtmlInputElement = by_id("input-add-group");
let on_click_add_group = EventListener::new(&button_add_group, "click", move |_event| {
EventListener::new(&button_add_group, "click", move |_event| {
let body = ron_api::Id { id: recipe_id };
spawn_local(async move {
let response: ron_api::Id = request::post("recipe/add_group", body).await.unwrap();
@ -325,8 +309,8 @@ pub fn setup_page(recipe_id: i64) -> Result<(), JsValue> {
steps: vec![],
});
});
});
on_click_add_group.forget();
})
.forget();
}
Ok(())
@ -397,7 +381,12 @@ fn create_group_element(group: &ron_api::Group) -> Element {
.selector::<HtmlInputElement>(".input-group-name")
.value();
spawn_local(async move {
if modal_dialog::show(&format!("Are you sure to delete the group '{}'", name)).await {
if modal_dialog::show(modal_dialog::DialogContent::<fn(&Element)>::Text(&format!(
"Are you sure to delete the group '{}'",
name
)))
.await
{
let body = ron_api::Id { id: group_id };
let _ = request::delete::<(), _>("recipe/remove_group", body).await;
let group_element = by_id::<Element>(&format!("group-{}", group_id));
@ -530,7 +519,12 @@ fn create_step_element(group_element: &Element, step: &ron_api::Step) -> Element
.selector::<HtmlTextAreaElement>(".text-area-step-action")
.value();
spawn_local(async move {
if modal_dialog::show(&format!("Are you sure to delete the step '{}'", action)).await {
if modal_dialog::show(modal_dialog::DialogContent::<fn(&Element)>::Text(&format!(
"Are you sure to delete the step '{}'",
action
)))
.await
{
let body = ron_api::Id { id: step_id };
let _ = request::delete::<(), _>("recipe/remove_step", body).await;
let step_element = by_id::<Element>(&format!("step-{}", step_id));
@ -675,12 +669,17 @@ fn create_ingredient_element(step_element: &Element, ingredient: &ron_api::Ingre
.selector::<HtmlInputElement>(".input-ingredient-name")
.value();
spawn_local(async move {
if modal_dialog::show(&format!("Are you sure to delete the ingredient '{}'", name))
.await
if modal_dialog::show(modal_dialog::DialogContent::<fn(&Element)>::Text(&format!(
"Are you sure to delete the ingredient '{}'",
name
)))
.await
{
let body = ron_api::Id { id: ingredient_id };
let _ = request::delete::<(), _>("recipe/remove_ingredient", body).await;
by_id::<Element>(&format!("ingredient-{}", ingredient_id)).remove();
let ingredient_element = by_id::<Element>(&format!("ingredient-{}", ingredient_id));
ingredient_element.next_element_sibling().unwrap().remove();
ingredient_element.remove();
}
});
})
@ -689,6 +688,22 @@ fn create_ingredient_element(step_element: &Element, ingredient: &ron_api::Ingre
ingredient_element
}
async fn reload_recipes_list(current_recipe_id: i64) {
match Request::get("/fragments/recipes_list")
.query([("current_recipe_id", current_recipe_id.to_string())])
.send()
.await
{
Err(error) => {
toast::show(Level::Info, &format!("Internal server error: {}", error));
}
Ok(response) => {
let list = document().get_element_by_id("recipes-list").unwrap();
list.set_outer_html(&response.text().await.unwrap());
}
}
}
enum CursorPosition {
UpperPart,
LowerPart,

View file

@ -0,0 +1,40 @@
use gloo::{
console::console,
events::EventListener,
net::http::Request,
utils::{document, window},
};
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::spawn_local;
use web_sys::{
DragEvent, Element, HtmlDivElement, HtmlInputElement, HtmlSelectElement, HtmlTextAreaElement,
KeyboardEvent,
};
use common::ron_api;
use crate::{
calendar, modal_dialog, request,
toast::{self, Level},
utils::{by_id, selector, selector_and_clone, SelectorExt},
};
pub fn setup_page(recipe_id: i64) -> Result<(), JsValue> {
let add_to_planner: Element = selector("#recipe-view .add-to-planner");
EventListener::new(&add_to_planner, "click", move |_event| {
// console!("CLICK".to_string());
spawn_local(async move {
modal_dialog::show(modal_dialog::DialogContent::CloneFromElement(
"#hidden-templates .calendar",
|element| {
// console!("SETUP...".to_string());
calendar::setup(element);
},
))
.await;
});
})
.forget();
Ok(())
}