use std::{cell::RefCell, rc::Rc}; use chrono::{Datelike, Days, Months, NaiveDate, Weekday, offset::Local}; use common::{ron_api, utils::substitute_with_names}; use gloo::{ console::log, events::EventListener, utils::{document, window}, }; use scanf::sscanf; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::spawn_local; use web_sys::{Element, HtmlInputElement}; use crate::{ modal_dialog, recipe_scheduler::RecipeScheduler, request, utils::{SelectorExt, by_id, get_locale, selector, selector_all}, }; struct CalendarStateInternal { displayed_date: NaiveDate, selected_date: NaiveDate, } #[derive(Clone)] pub struct CalendarState { internal_state: Rc>, } impl CalendarState { pub fn new() -> Self { let current_date = Local::now().date_naive(); Self { internal_state: Rc::new(RefCell::new(CalendarStateInternal { displayed_date: current_date, selected_date: current_date, })), } } pub fn displayed_date_next_month(&self) { let mut state_borrowed = self.internal_state.borrow_mut(); state_borrowed.displayed_date = state_borrowed.displayed_date + Months::new(1); } pub fn displayed_date_previous_month(&self) { let mut state_borrowed = self.internal_state.borrow_mut(); state_borrowed.displayed_date = state_borrowed.displayed_date - Months::new(1); } pub fn get_displayed_date(&self) -> NaiveDate { self.internal_state.borrow().displayed_date } pub fn get_selected_date(&self) -> NaiveDate { self.internal_state.borrow().selected_date } pub fn set_selected_date(&self, date: NaiveDate) { self.internal_state.borrow_mut().selected_date = date; } } #[derive(Clone, Copy)] pub struct CalendarOptions { pub can_select_date: bool, pub with_link_and_remove: bool, } pub fn setup( calendar: Element, options: CalendarOptions, recipe_scheduler: RecipeScheduler, ) -> CalendarState { let prev: Element = calendar.selector(".prev"); let next: Element = calendar.selector(".next"); let state = CalendarState::new(); display_month(&calendar, state.clone(), options, recipe_scheduler); // Click on previous month. let calendar_clone = calendar.clone(); let state_clone = state.clone(); EventListener::new(&prev, "click", move |_event| { state_clone.displayed_date_previous_month(); display_month( &calendar_clone, state_clone.clone(), options, recipe_scheduler, ); }) .forget(); // Click on next month. let calendar_clone = calendar.clone(); let state_clone = state.clone(); EventListener::new(&next, "click", move |_event| { state_clone.displayed_date_next_month(); display_month( &calendar_clone, state_clone.clone(), options, recipe_scheduler, ); }) .forget(); // Click on a day of the current month. let days: Element = calendar.selector(".days"); let calendar_clone = calendar.clone(); let state_clone = state.clone(); EventListener::new(&days, "click", move |event| { let target: Element = event.target().unwrap().dyn_into().unwrap(); // log!(event); // TODO: Remove. if target.class_name() == "number" && options.can_select_date { let first_day = first_grid_day(state_clone.get_displayed_date()); let day_grid_id = target.parent_element().unwrap().id(); let day_offset = day_grid_id[9..10].parse::().unwrap() * 7 + day_grid_id[10..11].parse::().unwrap(); state_clone.set_selected_date(first_day + Days::new(day_offset)); display_month( &calendar_clone, state_clone.clone(), options, recipe_scheduler, ); } else if target.class_name() == "remove-scheduled-recipe" { spawn_local(async move { let mut recipe_id: i64 = 0; let mut date: NaiveDate = NaiveDate::default(); sscanf!( &target.parent_element().unwrap().id(), "scheduled-recipe-{}-{}", recipe_id, date ) .unwrap(); let title = target.previous_element_sibling().unwrap().inner_html(); if let Some(remove_ingredients_from_shopping_list) = modal_dialog::show_and_initialize_with_ok( "#hidden-templates-calendar .unschedule-confirmation", async |element| { let date_format = selector::( "#hidden-templates-calendar .calendar-date-format", ) .inner_html(); element.set_inner_html(&substitute_with_names( &element.inner_html(), &["{title}", "{date}"], &[ &title, &date .format_localized(&date_format, get_locale()) .to_string(), ], )); }, |element, _| { let remove_ingredients_element: HtmlInputElement = element.selector("#input-remove-ingredients-from-shopping-list"); remove_ingredients_element.checked() }, ) .await { let body = ron_api::RemoveScheduledRecipe { recipe_id, date, remove_ingredients_from_shopping_list, }; let _ = request::delete::<(), _>("calendar/remove_scheduled_recipe", body).await; window().location().reload().unwrap(); } }); } }) .forget(); state } const NB_CALENDAR_ROW: u64 = 5; fn display_month( calendar: &Element, state: CalendarState, options: CalendarOptions, recipe_scheduler: RecipeScheduler, ) { let date = state.get_displayed_date(); calendar .selector::(".year") .set_inner_html(&date.year().to_string()); for (i, m) in calendar .selector_all::(".month") .into_iter() .enumerate() { if i as u32 + 1 == date.month() { m.set_class_name("month current"); } else { m.set_class_name("month"); } } let first_day = first_grid_day(date); let mut current = first_day; for i in 0..NB_CALENDAR_ROW { for j in 0..7 { let day_element: Element = by_id(&format!("day-grid-{}{}", i, j)); let day_content: Element = day_element.selector(".number"); day_content.set_inner_html(¤t.day().to_string()); let mut class_name = String::new(); if current.month() == date.month() { if !class_name.is_empty() { class_name += " "; } class_name += "current-month"; } if current == Local::now().date_naive() { if !class_name.is_empty() { class_name += " "; } class_name += "today"; } if options.can_select_date && current == state.get_selected_date() { if !class_name.is_empty() { class_name += " "; } class_name += "selected-day" } day_element.set_class_name(&class_name); current = current + Days::new(1); } } // Load and display scheduled recipes. spawn_local(async move { let scheduled_recipes = recipe_scheduler .get_scheduled_recipes(first_day, first_day + Days::new(NB_CALENDAR_ROW * 7)) .await .unwrap(); for scheduled_recipe in selector_all::(".scheduled-recipes") { scheduled_recipe.set_inner_html(""); } if !scheduled_recipes.is_empty() { let recipe_template: Element = if options.with_link_and_remove { selector("#hidden-templates-calendar .scheduled-recipe-with-link-and-remove") } else { selector("#hidden-templates-calendar .scheduled-recipe") }; for (date, title, recipe_id) in scheduled_recipes { let id = format!("scheduled-recipe-{}-{}", recipe_id, date); if document().get_element_by_id(&id).is_some() { continue; } let delta_from_first_day = (date - first_day).num_days(); let i = delta_from_first_day / 7; let j = delta_from_first_day % 7; let scheduled_recipes_element: Element = selector(&format!("#day-grid-{}{} .scheduled-recipes", i, j)); let recipe_element = recipe_template.deep_clone(); recipe_element.set_id(&id); scheduled_recipes_element .append_child(&recipe_element) .unwrap(); let recipe_link_element: Element = if options.with_link_and_remove { recipe_element.selector("a") } else { recipe_element }; if options.with_link_and_remove { recipe_link_element .set_attribute("href", &format!("/recipe/view/{}", recipe_id)) .unwrap(); } recipe_link_element.set_inner_html(&title); } } }); } fn first_grid_day(mut date: NaiveDate) -> NaiveDate { while (date - Days::new(1)).month() == date.month() { date = date - Days::new(1); } while date.weekday() != Weekday::Mon { date = date - Days::new(1); } date }