diff --git a/src/components/app.rs b/src/components/app.rs index 2673ec7..9565c68 100644 --- a/src/components/app.rs +++ b/src/components/app.rs @@ -24,7 +24,7 @@ const LOADING: Asset = asset!("/assets/loading.webp"); #[derive(Debug)] pub enum Action { Start(Track, DateTime), - FinishByStartNumber(StartNumber), + FinishByStartNumber(StartNumber, DateTime), } fn handle_rfid_event(selected_race: &mut Signal, event: rfid_reader::Event) { @@ -37,7 +37,7 @@ fn handle_rfid_event(selected_race: &mut Signal, event: rfid_reade info!("Tag {tag}"); selected_race.with_mut(|maybe_race| { if let Some(Ok(race)) = maybe_race { - race.tag_finished(&tag); + race.tag_finished(&tag, Utc::now()); } }); } @@ -53,10 +53,10 @@ fn handle_action(selected_race: &mut Signal, action: Action) { } }); } - Action::FinishByStartNumber(starting_number) => { + Action::FinishByStartNumber(starting_number, time) => { selected_race.with_mut(|maybe_race| { if let Some(Ok(race)) = maybe_race { - race.finish_start_number(starting_number); + race.finish_start_number(starting_number, time); } }); } diff --git a/src/components/manual_start_number_input.rs b/src/components/manual_start_number_input.rs index d7c9e60..1aa23a2 100644 --- a/src/components/manual_start_number_input.rs +++ b/src/components/manual_start_number_input.rs @@ -1,3 +1,4 @@ +use chrono::Utc; use dioxus::prelude::*; use crate::components::app::Action; @@ -13,7 +14,7 @@ pub fn ManualStartNumberInput() -> Element { event.prevent_default(); if let Ok(start_number) = start_number().parse() { use_coroutine_handle::() - .send(Action::FinishByStartNumber(start_number)); + .send(Action::FinishByStartNumber(start_number, Utc::now())); } start_number.set(String::from("")); }, diff --git a/src/components/mod.rs b/src/components/mod.rs index cd59de7..2556f92 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1,6 +1,7 @@ pub mod app; pub mod categories_list; pub mod manual_start_number_input; +pub mod racer_row; pub mod racers; pub mod races_list; pub mod th; diff --git a/src/components/racer_row.rs b/src/components/racer_row.rs new file mode 100644 index 0000000..89d63fb --- /dev/null +++ b/src/components/racer_row.rs @@ -0,0 +1,44 @@ +use dioxus::prelude::*; + +use crate::components::app::Action; +use crate::components::time_input::TimeInput; +use crate::race::Racer; +use crate::time_utils::{format_time, format_time_delta}; + +#[component] +pub fn RacerRow(racer: Racer) -> Element { + let editing = use_signal(|| false); + let start_number = racer.start_number.clone(); + + rsx! { + tr { + td { "{racer.start_number}" } + td { "{racer.first_name}" } + td { "{racer.last_name}" } + td { "{racer.track}" } + td { "{format_time(racer.start)}" } + td { width: "124px", + TimeInput { + time: racer.finish, + editing, + onsave: move |time| { + use_coroutine_handle::() + .send(Action::FinishByStartNumber(start_number.clone(), time)); + }, + } + } + td { "{format_time_delta(racer.time)}" } + td { "{racer.track_rank.map(|rank| rank.to_string()).unwrap_or_default() }" } + td { + for category in racer.categories.clone() { + "{category} " + } + } + td { + for category_rank in &racer.categories_rank { + span { class: "me-2", "{category_rank.0}: {category_rank.1}" } + } + } + } + } +} diff --git a/src/components/racers.rs b/src/components/racers.rs index bcfd2eb..a2fe30e 100644 --- a/src/components/racers.rs +++ b/src/components/racers.rs @@ -1,39 +1,15 @@ -use chrono::{DateTime, Local, TimeDelta, Utc}; use dioxus::prelude::*; use crate::{ components::{ categories_list::CategoriesList, + racer_row::RacerRow, th::{Sorter, Th}, }, race::{Category, Race, Racer, RacerField}, + time_utils::{format_time, format_time_delta}, }; -fn format_time(datetime: Option>) -> String { - match datetime { - Some(datetime) => datetime - .with_timezone(&Local) - .format("%H:%M:%S%.3f") - .to_string(), - None => "".to_string(), - } -} - -pub fn format_time_delta(delta: Option) -> String { - let delta = match delta { - Some(delta) => delta, - _ => return "".to_string(), - }; - - let total_millis = delta.num_milliseconds(); - let hours = total_millis / 1000 / 3600; - let mins = (total_millis / 1000 / 60) % 60; - let secs = (total_millis / 1000) % 60; - let millis = total_millis % 1000; - - format!("{hours:02}:{mins:02}:{secs:02}.{millis:03}") -} - // Checks whether a racer matches all active filters in the map. // The `filters` map contains RacerField -> Option. Only non-empty Some() // values are applied. Matching is case-insensitive for string like types. @@ -180,26 +156,7 @@ pub fn Racers(race: Race) -> Element { .iter() .filter(|racer| matches_filters(racer, &filters(), selected_category_id())) { - tr { - td { "{racer.start_number}" } - td { "{racer.first_name}" } - td { "{racer.last_name}" } - td { "{racer.track}" } - td { "{format_time(racer.start)}" } - td { "{format_time(racer.finish)}" } - td { "{format_time_delta(racer.time)}" } - td { "{racer.track_rank.map(|rank| rank.to_string()).unwrap_or_default() }" } - td { - for category in racer.categories.clone() { - "{category} " - } - } - td { - for category_rank in &racer.categories_rank { - span { class: "me-2", "{category_rank.0}: {category_rank.1}" } - } - } - } + RacerRow { racer: racer.clone() } } } } diff --git a/src/components/time_input.rs b/src/components/time_input.rs index d893357..3c72067 100644 --- a/src/components/time_input.rs +++ b/src/components/time_input.rs @@ -21,6 +21,7 @@ pub fn TimeInput( time: Option>, editing: Signal, onsave: EventHandler>, + span_class: Option, ) -> Element { let mut text = use_signal(|| "".to_string()); @@ -54,7 +55,7 @@ pub fn TimeInput( } } else { span { - class: "input-group-text flex-grow-1", + class: span_class, ondoubleclick: move |_evt| { editing.set(true); }, diff --git a/src/components/track_start.rs b/src/components/track_start.rs index 8bee81c..9b8bc19 100644 --- a/src/components/track_start.rs +++ b/src/components/track_start.rs @@ -21,6 +21,7 @@ pub fn TrackStart(track: Track, start: Option>) -> Element { TimeInput { time: start, editing, + span_class: "input-group-text flex-grow-1", onsave: move |time| { use_coroutine_handle::().send(Action::Start(track.clone(), time)); }, diff --git a/src/main.rs b/src/main.rs index 235f6dc..98aca48 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,6 +14,7 @@ mod race; mod race_events; mod restclient; mod rfid_reader; +mod time_utils; const MAIN_CSS: Asset = asset!("/assets/main.css"); const BOOTSTRAP_CSS: Asset = asset!("/assets/bootstrap.css"); diff --git a/src/printer.rs b/src/printer.rs index c521b13..cbd5cbf 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -1,5 +1,5 @@ -use crate::components::racers::format_time_delta; use crate::race::{Race, Racer}; +use crate::time_utils::format_time_delta; use std::path::PathBuf; use tracing::{error, info}; diff --git a/src/race.rs b/src/race.rs index cbaf588..02b1ac6 100644 --- a/src/race.rs +++ b/src/race.rs @@ -249,34 +249,39 @@ impl Race { self.track_starts.insert(track, time); } - fn finish(&mut self, mut predicate: F) -> Result<(), ()> + fn finish(&mut self, mut predicate: F, time: DateTime) -> Result<(), ()> where F: for<'a> FnMut(&&'a mut Racer) -> bool, { let racer = self.racers.iter_mut().find(|r| predicate(r)).ok_or(())?; - let finish_time = Utc::now(); - racer.finish = Some(finish_time); + racer.finish = Some(time); racer.time = calculate_time(racer.start, racer.finish); self.log .borrow_mut() - .log_finish(racer.start_number.clone(), finish_time); + .log_finish(racer.start_number.clone(), time); self.map_start_number_to_track_rank(); self.map_start_number_to_categories_rank(); Ok(()) } - pub fn finish_start_number(&mut self, start_number: StartNumber) { + pub fn finish_start_number(&mut self, start_number: StartNumber, time: DateTime) { if self - .finish(|r| r.start_number == start_number && r.start.is_some() && r.finish.is_none()) + .finish( + |r| r.start_number == start_number && r.start.is_some(), + time, + ) .is_err() { error!("Racer with starting number {start_number:?} not found."); } } - pub fn tag_finished(&mut self, tag: &str) { + pub fn tag_finished(&mut self, tag: &str, time: DateTime) { if self - .finish(|r| r.tag == tag && r.start.is_some() && r.finish.is_none()) + .finish( + |r| r.tag == tag && r.start.is_some() && r.finish.is_none(), + time, + ) .is_err() { error!("Racer with tag {tag} not found."); diff --git a/src/time_utils.rs b/src/time_utils.rs new file mode 100644 index 0000000..af7d425 --- /dev/null +++ b/src/time_utils.rs @@ -0,0 +1,26 @@ +use chrono::{DateTime, Local, TimeDelta, Utc}; + +pub fn format_time(datetime: Option>) -> String { + match datetime { + Some(datetime) => datetime + .with_timezone(&Local) + .format("%H:%M:%S%.3f") + .to_string(), + None => "".to_string(), + } +} + +pub fn format_time_delta(delta: Option) -> String { + let delta = match delta { + Some(delta) => delta, + _ => return "".to_string(), + }; + + let total_millis = delta.num_milliseconds(); + let hours = total_millis / 1000 / 3600; + let mins = (total_millis / 1000 / 60) % 60; + let secs = (total_millis / 1000) % 60; + let millis = total_millis % 1000; + + format!("{hours:02}:{mins:02}:{secs:02}.{millis:03}") +}