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

19
Cargo.lock generated
View file

@ -149,9 +149,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "axum"
version = "0.8.2"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efea76243612a2436fb4074ba0cf3ba9ea29efdeb72645d8fc63f116462be1de"
checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8"
dependencies = [
"axum-core",
"axum-macros",
@ -184,12 +184,12 @@ dependencies = [
[[package]]
name = "axum-core"
version = "0.5.1"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eab1b0df7cded837c40dacaa2e1c33aa17c84fc3356ae67b5645f1e83190753e"
checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733"
dependencies = [
"bytes",
"futures-core",
"futures-util",
"http 1.2.0",
"http-body",
"http-body-util",
@ -204,9 +204,9 @@ dependencies = [
[[package]]
name = "axum-extra"
version = "0.11.0"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "543f0799d22486525744f06a3580b64f3e51d97aba73ea0e09040969c0034722"
checksum = "460fc6f625a1f7705c6cf62d0d070794e94668988b1c38111baeec177c715f7b"
dependencies = [
"axum",
"axum-core",
@ -700,6 +700,7 @@ dependencies = [
name = "frontend"
version = "0.1.0"
dependencies = [
"chrono",
"common",
"console_error_panic_hook",
"futures",
@ -2067,9 +2068,9 @@ checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497"
[[package]]
name = "rustix"
version = "0.38.43"
version = "0.38.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6"
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
dependencies = [
"bitflags",
"errno",

View file

@ -8,7 +8,7 @@ edition = "2021"
common = { path = "../common" }
axum = { version = "0.8", features = ["macros"] }
axum-extra = { version = "0.11", features = ["cookie"] }
axum-extra = { version = "0.10", features = ["cookie"] }
tokio = { version = "1", features = ["full"] }
tower = { version = "0.5", features = ["util"] }
tower-http = { version = "0.6", features = ["fs", "trace"] }

View file

@ -0,0 +1,46 @@
.calendar {
.month-selector {
width: 100%;
text-align: center;
.prev {
float: left;
}
.next {
float: right;
}
.month {
display: none;
}
.month.current {
display: inline;
}
}
ul.weekdays {
margin: 0;
padding: 20px 0;
li {
display: inline-block;
width: 14%;
text-align: center;
margin: 0;
}
}
ul.days {
margin: 0;
padding: 20px 0;
li {
display: inline-block;
width: 14%;
text-align: center;
margin: 0;
}
}
}

View file

@ -1,8 +1,8 @@
#modal-dialog {
// visibility: hidden;
color: white;
max-width: 300px;
margin-left: -125px;
width: 500px;
margin-left: -250px;
background-color: black;
text-align: center;
border-radius: 2px;

View file

@ -1,5 +1,6 @@
@use 'toast.scss';
@use 'modal-dialog.scss';
@use 'calendar.scss';
$color-1: #B29B89;
$color-2: #89B29B;
@ -123,6 +124,10 @@ body {
h1 {
text-align: center;
}
#hidden-templates {
display: none;
}
}
#recipe-edit {
@ -163,10 +168,6 @@ body {
background-color: red;
}
}
#hidden-templates {
display: none;
}
}
form {

View file

@ -1,4 +1,4 @@
use std::{fs::File, sync::LazyLock};
use std::{borrow::Borrow, fs::File, sync::LazyLock};
use ron::de::from_reader;
use serde::Deserialize;
@ -114,6 +114,27 @@ pub enum Sentence {
RecipeOneServing,
RecipeSomeServings,
RecipeEstimatedTimeMinAbbreviation,
// Calendar.
CalendarMondayAbbreviation,
CalendarTuesdayAbbreviation,
CalendarWednesdayAbbreviation,
CalendarThursdayAbbreviation,
CalendarFridayAbbreviation,
CalendarSaturdayAbbreviation,
CalendarSundayAbbreviation,
CalendarJanuary,
CalendarFebruary,
CalendarMarch,
CalendarApril,
CalendarMay,
CalendarJune,
CalendarJuly,
CalendarAugust,
CalendarSeptember,
CalendarOctober,
CalendarNovember,
CalendarDecember,
}
pub const DEFAULT_LANGUAGE_CODE: &str = "en";
@ -131,7 +152,10 @@ impl Tr {
}
}
pub fn t(&self, sentence: Sentence) -> &'static str {
pub fn t<T>(&self, sentence: T) -> &'static str
where
T: Borrow<Sentence>,
{
self.lang.get(sentence)
}
@ -196,10 +220,15 @@ impl Language {
}
}
pub fn get(&'static self, sentence: Sentence) -> &'static str {
pub fn get<T>(&'static self, sentence: T) -> &'static str
where
T: Borrow<Sentence>,
{
let sentence_cloned: Sentence = sentence.borrow().clone();
let text: &str = self
.translation
.get(sentence.clone() as usize)
.get(sentence_cloned as usize)
.unwrap()
.as_ref();
if text.is_empty() && self.code != DEFAULT_LANGUAGE_CODE {

View file

@ -0,0 +1,45 @@
<div class="calendar">
<div class="month-selector">
<span class="prev">PREV</span>
<span class="year" ></span>
{% for month in [
Sentence::CalendarJanuary,
Sentence::CalendarFebruary,
Sentence::CalendarMarch,
Sentence::CalendarApril,
Sentence::CalendarMay,
Sentence::CalendarJune,
Sentence::CalendarJuly,
Sentence::CalendarAugust,
Sentence::CalendarSeptember,
Sentence::CalendarOctober,
Sentence::CalendarNovember,
Sentence::CalendarDecember,
] %}
<span class="month">{{ tr.t(*month) }}</span>
{% endfor %}
<span class="next">NEXT</span>
</div>
<ul class="weekdays">
{% for day in [
Sentence::CalendarMondayAbbreviation,
Sentence::CalendarTuesdayAbbreviation,
Sentence::CalendarWednesdayAbbreviation,
Sentence::CalendarThursdayAbbreviation,
Sentence::CalendarFridayAbbreviation,
Sentence::CalendarSaturdayAbbreviation,
Sentence::CalendarSundayAbbreviation,
] %}
<li class="weekday">{{ tr.t(*day) }}</li>
{% endfor %}
<ul class="days">
{% for i in 0..7 %}
{% for j in 0..5 %}
<li id="day-{{i}}{{j}}"></li>
{% endfor %}
{% endfor %}
</ul>
</div>

View file

@ -5,9 +5,12 @@
<div class="content" id="recipe-view">
<h2 class="recipe-title" >{{ recipe.title }}</h2>
{% if user.is_some() && crate::data::model::can_user_edit_recipe(&user.as_ref().unwrap(), &recipe) %}
{% if let Some(user) = user %}
{% if crate::data::model::can_user_edit_recipe(user, recipe) %}
<a class="edit-recipe" href="/recipe/edit/{{ recipe.id }}" >Edit</a>
{% endif %}
<span class="add-to-planner">Add to planner</span>
{% endif %}
<div class="tags">
{% for tag in recipe.tags %}
@ -27,7 +30,6 @@
{% else %}
{% endmatch %}
{% match recipe.estimated_time %}
{% when Some(time) %}
{{ time +}} {{+ tr.t(Sentence::RecipeEstimatedTimeMinAbbreviation) }}
@ -76,6 +78,10 @@
</div>
</div>
{% endfor %}
<div id="hidden-templates">
{% include "calendar.html" %}
</div>
</div>
{% endblock %}

View file

@ -99,6 +99,26 @@
(RecipeOneServing, "1 serving"),
(RecipeSomeServings, "{} servings"),
(RecipeEstimatedTimeMinAbbreviation, "min"),
(CalendarMondayAbbreviation, "Mon"),
(CalendarTuesdayAbbreviation, "Tue"),
(CalendarWednesdayAbbreviation, "Wed"),
(CalendarThursdayAbbreviation, "Thu"),
(CalendarFridayAbbreviation, "Fri"),
(CalendarSaturdayAbbreviation, "Sat"),
(CalendarSundayAbbreviation, "Sun"),
(CalendarJanuary, "January"),
(CalendarFebruary, "February"),
(CalendarMarch, "March"),
(CalendarApril, "April"),
(CalendarMay, "May"),
(CalendarJune, "June"),
(CalendarJuly, "July"),
(CalendarAugust, "August"),
(CalendarSeptember, "September"),
(CalendarOctober, "October"),
(CalendarNovember, "November"),
(CalendarDecember, "December"),
]
),
(
@ -201,6 +221,26 @@
(RecipeOneServing, "pour 1 personne"),
(RecipeSomeServings, "pour {} personnes"),
(RecipeEstimatedTimeMinAbbreviation, "min"),
(CalendarMondayAbbreviation, "Lun"),
(CalendarTuesdayAbbreviation, "Mar"),
(CalendarWednesdayAbbreviation, "Mer"),
(CalendarThursdayAbbreviation, "Jeu"),
(CalendarFridayAbbreviation, "Ven"),
(CalendarSaturdayAbbreviation, "Sam"),
(CalendarSundayAbbreviation, "Dim"),
(CalendarJanuary, "Janvier"),
(CalendarFebruary, "Février"),
(CalendarMarch, "Mars"),
(CalendarApril, "Avril"),
(CalendarMay, "Mai"),
(CalendarJune, "Juin"),
(CalendarJuly, "Juillet"),
(CalendarAugust, "Août"),
(CalendarSeptember, "Septembre"),
(CalendarOctober, "Octobre"),
(CalendarNovember, "Novembre"),
(CalendarDecember, "Décembre"),
]
)
]

View file

@ -13,6 +13,8 @@ default = ["console_error_panic_hook"]
[dependencies]
common = { path = "../common" }
chrono = "0.4"
ron = "0.8"
serde = { version = "1.0", features = ["derive"] }
thiserror = "2"

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,13 +22,21 @@ 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[..] {
// 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.
}
["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)?;
// }

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))
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(())
}