[[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",
[[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",
[[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",
name = "frontend"
version = "0.1.0"
dependencies = [
+ "chrono",
"common",
"console_error_panic_hook",
"futures",
[[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",
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"] }
--- /dev/null
+.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;
+ }
+ }
+}
\ No newline at end of file
#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;
@use 'toast.scss';
@use 'modal-dialog.scss';
+@use 'calendar.scss';
$color-1: #B29B89;
$color-2: #89B29B;
h1 {
text-align: center;
}
+
+ #hidden-templates {
+ display: none;
+ }
}
#recipe-edit {
background-color: red;
}
}
-
- #hidden-templates {
- display: none;
- }
}
form {
-use std::{fs::File, sync::LazyLock};
+use std::{borrow::Borrow, fs::File, sync::LazyLock};
use ron::de::from_reader;
use serde::Deserialize;
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";
}
}
- 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)
}
}
}
- 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 {
--- /dev/null
+<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>
\ No newline at end of file
<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) %}
- <a class="edit-recipe" href="/recipe/edit/{{ recipe.id }}" >Edit</a>
+ {% 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">
{% else %}
{% endmatch %}
-
{% match recipe.estimated_time %}
{% when Some(time) %}
{{ time +}} {{+ tr.t(Sentence::RecipeEstimatedTimeMinAbbreviation) }}
</div>
</div>
{% endfor %}
+
+ <div id="hidden-templates">
+ {% include "calendar.html" %}
+ </div>
</div>
{% endblock %}
\ No newline at end of file
(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"),
]
),
(
(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"),
]
)
]
\ No newline at end of file
[dependencies]
common = { path = "../common" }
+chrono = "0.4"
+
ron = "0.8"
serde = { version = "1.0", features = ["derive"] }
thiserror = "2"
--- /dev/null
+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(¤t.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);
+ }
+ }
+}
+mod calendar;
mod modal_dialog;
mod on_click;
mod recipe_edit;
+mod recipe_view;
mod request;
mod toast;
mod utils;
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");
use futures::{future::FutureExt, pin_mut, select};
use web_sys::{Element, HtmlDialogElement};
-use crate::utils::{by_id, SelectorExt};
-
-use crate::on_click;
+use crate::{
+ on_click,
+ utils::{by_id, selector_and_clone, SelectorExt},
+};
+
+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();
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.
{
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 };
// 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();
steps: vec![],
});
});
- });
- on_click_add_group.forget();
+ })
+ .forget();
}
Ok(())
.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));
.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));
.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();
}
});
})
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,
--- /dev/null
+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(())
+}