diff --git a/crates/frontend/src/components/root.rs b/crates/frontend/src/components/root.rs index 92480e1..13709f8 100644 --- a/crates/frontend/src/components/root.rs +++ b/crates/frontend/src/components/root.rs @@ -1,5 +1,7 @@ use crate::components::achievement::AchievementComponent; use crate::components::milestone::MilestoneComponent; +use chrono::NaiveTime; +use common::Achievement; use yew::functional::*; use yew::prelude::*; use yew_router::prelude::*; @@ -20,19 +22,41 @@ pub fn Root() -> Html { nav.push(&crate::Route::CreateMilestone); }); - let achievements = app_state - .state - .achievements - .iter() - .cloned() - .enumerate() - .map(|(idx, a)| (idx + 1, a)) - .map(|(n, a)| { - html! { - + let current_time: chrono::NaiveTime = chrono::Local::now().time(); + + let achievements = app_state.state.achievements.clone(); + let grouped_achievements = group_achievements(achievements); + + let mut achievements_html: Vec = vec![]; + let mut n = 0; + for (time_of_reveal, group) in grouped_achievements { + if let Some(time_of_reveal) = time_of_reveal { + let s = if time_of_reveal > current_time { + format!( + "{} more revealed at {}!", + group.len(), + time_of_reveal.format("%H:%M") + ) + } else { + format!("{}", time_of_reveal.format("%H:%M")) + }; + let html = html! { +

{s}

+ }; + achievements_html.push(html); + if time_of_reveal > current_time { + break; } - }) - .collect::(); + } + for a in group { + n += 1; + let html = html! { + + }; + achievements_html.push(html); + } + } + let achievements = achievements_html.into_iter().collect::(); let completed_achievements = app_state .state @@ -77,3 +101,139 @@ pub fn Root() -> Html { } } + +fn group_achievements(mut achievements: Vec) -> Vec<(Option, Vec)> { + // Sort the achievements by time_of_reveal. + achievements.sort_by_key(|a| a.time_of_reveal); + + let mut grouped_achievements: Vec<(Option, Vec)> = Vec::new(); + let mut current_time_of_reveal: Option = None; + let mut current_group: Vec = Vec::new(); + + for achievement in achievements { + if current_time_of_reveal == achievement.time_of_reveal { + current_group.push(achievement); + continue; + } + if !current_group.is_empty() { + grouped_achievements.push((current_time_of_reveal, current_group)); + } + current_time_of_reveal = achievement.time_of_reveal; + current_group = vec![achievement]; + } + + if !current_group.is_empty() { + grouped_achievements.push((current_time_of_reveal, current_group)); + } + + grouped_achievements +} + +#[cfg(test)] +mod tests { + use super::*; + use uuid::Uuid; + + #[test] + fn test_group_achievements() { + let time1 = NaiveTime::from_hms_opt(12, 0, 0).unwrap(); + let time2 = NaiveTime::from_hms_opt(13, 0, 0).unwrap(); + + let mut achievements = vec![ + Achievement { + goal: "Goal 1".to_string(), + completed: true, + uuid: Uuid::new_v4(), + time_of_reveal: Some(time1), + }, + Achievement { + goal: "Goal 2".to_string(), + completed: false, + uuid: Uuid::new_v4(), + time_of_reveal: Some(time1), + }, + Achievement { + goal: "Goal 3".to_string(), + completed: true, + uuid: Uuid::new_v4(), + time_of_reveal: Some(time2), + }, + ]; + + let grouped = group_achievements(achievements); + assert_eq!(grouped.len(), 2); + assert_eq!(grouped[0].0, Some(time1)); + assert_eq!(grouped[0].1.len(), 2); + assert_eq!(grouped[1].0, Some(time2)); + assert_eq!(grouped[1].1.len(), 1); + } + + #[test] + fn test_group_achievements_empty_vec() { + let achievements = vec![]; + let grouped = group_achievements(achievements); + assert_eq!(grouped.len(), 0); + } + + #[test] + fn test_group_achievements_group_none_values() { + let time1 = NaiveTime::from_hms_opt(12, 0, 0).unwrap(); + + let achievements = vec![ + Achievement { + goal: "Goal 1".to_string(), + completed: true, + uuid: Uuid::new_v4(), + time_of_reveal: Some(time1), + }, + Achievement { + goal: "Goal 2".to_string(), + completed: false, + uuid: Uuid::new_v4(), + time_of_reveal: None, + }, + Achievement { + goal: "Goal 3".to_string(), + completed: true, + uuid: Uuid::new_v4(), + time_of_reveal: None, + }, + ]; + + let grouped = group_achievements(achievements); + assert_eq!(grouped.len(), 2); + assert_eq!(grouped[0].0, None); + assert_eq!(grouped[0].1.len(), 2); + assert_eq!(grouped[1].0, Some(time1)); + assert_eq!(grouped[1].1.len(), 1); + } + + #[test] + fn test_group_achievements_all_none() { + let achievements = vec![ + Achievement { + goal: "Goal 1".to_string(), + completed: true, + uuid: Uuid::new_v4(), + time_of_reveal: None, + }, + Achievement { + goal: "Goal 2".to_string(), + completed: false, + uuid: Uuid::new_v4(), + time_of_reveal: None, + }, + Achievement { + goal: "Goal 3".to_string(), + completed: true, + uuid: Uuid::new_v4(), + time_of_reveal: None, + }, + ]; + + let grouped = group_achievements(achievements); + assert_eq!(grouped.len(), 1); + assert_eq!(grouped[0].0, None); + assert_eq!(grouped[0].1.len(), 3); + } +} diff --git a/todo.md b/todo.md index e9de734..2382e8a 100644 --- a/todo.md +++ b/todo.md @@ -1,3 +1,5 @@ - UI errors from failed requests - websocket reconnect - disable timed reveal +- trigger some refresh when time exceeds next reveal +- make it an admin interface, and move the new + remove buttons there, as well as milestone management