diff --git a/frontend/assets/index.scss b/frontend/assets/index.scss index fe4d322..6855a66 100644 --- a/frontend/assets/index.scss +++ b/frontend/assets/index.scss @@ -193,3 +193,15 @@ div.is-calendar { .is-scrollbar-width-none { scrollbar-width: none; } + +// Tags + +.tags.has-addons .tag:not(:first-child) { + border-left: 1px solid; +} + +// Dropdown menu + +.dropdown-menu.has-no-min-width { + min-width: auto; +} diff --git a/frontend/src/common.rs b/frontend/src/common.rs index 574adaa..03df077 100644 --- a/frontend/src/common.rs +++ b/frontend/src/common.rs @@ -1087,32 +1087,76 @@ where ] } -pub fn view_sets_per_muscle(stimulus_per_muscle: &[(&str, u32)]) -> Node +pub fn view_sets_per_muscle(stimulus_per_muscle: &[(domain::Muscle, u32)]) -> Vec> +where + Ms: 'static, +{ + let mut stimulus_per_muscle = stimulus_per_muscle.to_vec(); + stimulus_per_muscle.sort_by(|a, b| b.1.cmp(&a.1)); + let mut groups = vec![vec![], vec![], vec![], vec![]]; + for (muscle, stimulus) in stimulus_per_muscle { + let name = domain::Muscle::name(muscle); + let description = domain::Muscle::description(muscle); + let sets = f64::from(stimulus) / 100.0; + let sets_str = format!("{:.1$}", sets, usize::from(sets.fract() != 0.0)); + if sets > 10.0 { + groups[0].push((name, description, sets_str, vec!["is-dark"])); + } else if sets >= 3.0 { + groups[1].push((name, description, sets_str, vec!["is-dark", "is-link"])); + } else if sets > 0.0 { + groups[2].push((name, description, sets_str, vec!["is-light", "is-link"])); + } else { + groups[3].push((name, description, sets_str, vec!["is-light"])); + } + } + groups + .iter() + .filter(|g| !g.is_empty()) + .map(|g| view_tags_with_addons(g)) + .collect::>() +} + +fn view_tags_with_addons(tags: &[(&str, &str, String, Vec<&str>)]) -> Node where Ms: 'static, { div![ - C!["table-container"], - table![ - C!["table"], - C!["is-flex"], - C!["has-text-centered"], - tbody![ - C!["mx-auto"], - stimulus_per_muscle - .iter() - .map(|(name, stimulus)| { - let sets = f64::from(*stimulus) / 100.0; - tr![ - td![C!["is-borderless"], C!["py-1"], name], - td![ - C!["is-borderless"], - C!["py-1"], - format!("{:.1$}", sets, usize::from(sets.fract() != 0.0)) - ] - ] - }) - .collect::>(), + C!["field"], + C!["is-grouped"], + C!["is-grouped-multiline"], + C!["is-justify-content-center"], + C!["mx-2"], + tags.iter().map(|(name, description, value, attributes)| { + view_element_with_description( + div![ + C!["tags"], + C!["has-addons"], + span![C!["tag"], attributes.iter().map(|a| C![a]), name], + span![C!["tag"], attributes.iter().map(|a| C![a]), value] + ], + description, + ) + }) + ] +} + +pub fn view_element_with_description(element: Node, description: &str) -> Node { + div![ + C!["dropdown"], + C!["is-hoverable"], + div![ + C!["dropdown-trigger"], + div![C!["control"], C!["is-clickable"], element] + ], + IF![ + not(description.is_empty()) => + div![ + C!["dropdown-menu"], + C!["has-no-min-width"], + div![ + C!["dropdown-content"], + div![C!["dropdown-item"], description] + ] ] ] ] diff --git a/frontend/src/data.rs b/frontend/src/data.rs index 6dc6cc5..6e2cee3 100644 --- a/frontend/src/data.rs +++ b/frontend/src/data.rs @@ -279,10 +279,14 @@ impl Routine { } pub fn stimulus_per_muscle(&self, exercises: &BTreeMap) -> BTreeMap { - let mut result: BTreeMap = BTreeMap::new(); + let mut result: BTreeMap = domain::Muscle::iter() + .map(|m| (domain::Muscle::id(*m), 0)) + .collect(); for section in &self.sections { for (id, stimulus) in section.stimulus_per_muscle(exercises) { - *result.entry(id).or_insert(0) += stimulus; + if result.contains_key(&id) { + *result.entry(id).or_insert(0) += stimulus; + } } } result diff --git a/frontend/src/page/exercise.rs b/frontend/src/page/exercise.rs index 99acf39..4e38769 100644 --- a/frontend/src/page/exercise.rs +++ b/frontend/src/page/exercise.rs @@ -422,12 +422,15 @@ fn view_muscles(model: &Model) -> Node { C!["mx-2"], C!["mb-5"], muscles.iter().map(|(m, stimulus)| { - span![ - C!["tag"], - C!["is-link"], - C![IF![*stimulus < 100 => "is-light"]], - domain::Muscle::name(**m) - ] + common::view_element_with_description( + span![ + C!["tag"], + C!["is-link"], + C![IF![*stimulus < 100 => "is-light"]], + domain::Muscle::name(**m) + ], + domain::Muscle::description(**m), + ) }) ] } diff --git a/frontend/src/page/routine.rs b/frontend/src/page/routine.rs index e130c5c..a86a757 100644 --- a/frontend/src/page/routine.rs +++ b/frontend/src/page/routine.rs @@ -1261,14 +1261,8 @@ fn view_muscles(routine: &data::Routine, data_model: &data::Model) -> Node let stimulus_per_muscle = routine .stimulus_per_muscle(&data_model.exercises) .iter() - .map(|(id, stimulus)| { - ( - domain::Muscle::from_repr(*id) - .as_ref() - .map(|m| domain::Muscle::name(*m)) - .unwrap_or_default(), - *stimulus, - ) + .filter_map(|(id, stimulus)| { + domain::Muscle::from_repr(*id).map(|muscle| (muscle, *stimulus)) }) .collect::>(); if stimulus_per_muscle.is_empty() { diff --git a/frontend/src/page/training_session.rs b/frontend/src/page/training_session.rs index 618b345..1f2f95e 100644 --- a/frontend/src/page/training_session.rs +++ b/frontend/src/page/training_session.rs @@ -1950,14 +1950,8 @@ fn view_muscles(training_session: &data::TrainingSession, data_model: &data::Mod let stimulus_per_muscle = training_session .stimulus_per_muscle(&data_model.exercises) .iter() - .map(|(id, stimulus)| { - ( - domain::Muscle::from_repr(*id) - .as_ref() - .map(|m| domain::Muscle::name(*m)) - .unwrap_or_default(), - *stimulus, - ) + .filter_map(|(id, stimulus)| { + domain::Muscle::from_repr(*id).map(|muscle| (muscle, *stimulus)) }) .collect::>(); if stimulus_per_muscle.is_empty() { diff --git a/tests/e2e/web_test.py b/tests/e2e/web_test.py index 422468a..2da3ffa 100644 --- a/tests/e2e/web_test.py +++ b/tests/e2e/web_test.py @@ -490,7 +490,7 @@ def test_training_session(driver: webdriver.Chrome) -> None: page.load() page.wait_for_title(str(workout.date)) - assert page.get_sets() == sets + assert page.get_sets() == [*sets, "Pecs", "2"] page.edit()