Added add time system

This commit is contained in:
xeovalyte 2024-04-08 15:58:58 +02:00
parent 465f4a5673
commit 067a0860ab
No known key found for this signature in database
4 changed files with 262 additions and 20 deletions

View File

@ -28,6 +28,8 @@ once_cell = "1.19.0"
futures = "0.3.30" futures = "0.3.30"
uuid = "1.8.0" uuid = "1.8.0"
leptos-use = "0.10.6" leptos-use = "0.10.6"
strsim = "0.11.1"
web-sys = { version = "0.3.69", features = ["Document", "Window", "Element", "ScrollIntoViewOptions", "ScrollLogicalPosition", "ScrollBehavior" ] }
[features] [features]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"] hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
@ -39,6 +41,7 @@ ssr = [
"dep:tower-http", "dep:tower-http",
"dep:leptos_axum", "dep:leptos_axum",
"leptos/ssr", "leptos/ssr",
"leptos-use/ssr",
"leptos_meta/ssr", "leptos_meta/ssr",
"leptos_router/ssr", "leptos_router/ssr",
"dep:tracing", "dep:tracing",

View File

@ -1,32 +1,53 @@
use crate::util::surrealdb::schemas; use crate::util::surrealdb::schemas;
use leptos::*; use leptos::{ev::keydown, *};
use leptos_router::ActionForm; use leptos_router::{ActionForm, FromFormData};
use leptos_use::*;
use strsim::normalized_damerau_levenshtein;
use web_sys::ScrollIntoViewOptions;
cfg_if::cfg_if! { cfg_if::cfg_if! {
if #[cfg(feature = "ssr")] { if #[cfg(feature = "ssr")] {
use crate::util::surrealdb::{DB}; use crate::util::surrealdb::{DB};
use surrealdb::opt::PatchOp;
use leptos::logging; use leptos::logging;
} }
} }
#[derive(Clone)]
struct Time {
minutes: u32,
seconds: u32,
milliseconds: u32,
}
impl Time {
fn as_milliseconds(&self) -> u32 {
self.minutes * 60 * 1000 + self.seconds * 1000 + self.milliseconds * 10
}
}
#[server(AddTime)] #[server(AddTime)]
async fn add_time(name: String, group: String) -> Result<(), ServerFnError> { async fn add_time(
let created: Vec<schemas::Participant> = DB mut participant: schemas::Participant,
.create("participant") event: String,
.content(schemas::NewParticipant { name, group }) time: u32,
) -> Result<(), ServerFnError> {
let updated: Option<schemas::ParticipantRecord> = DB
.update(("participant", participant.id))
.patch(PatchOp::replace(&("/events/".to_owned() + &event), time))
.await?; .await?;
match created.first() { match updated {
Some(participant) => { Some(participant) => {
logging::log!( logging::log!(
"Created participant: {} ({})", "Updated participant: {} ({})",
participant.name, participant.name,
participant.group participant.group
); );
Ok(()) Ok(())
} }
None => Err(ServerFnError::ServerError(String::from( None => Err(ServerFnError::ServerError(String::from(
"Could not create participant", "Could not update participant",
))), ))),
} }
} }
@ -34,17 +55,191 @@ async fn add_time(name: String, group: String) -> Result<(), ServerFnError> {
/// Renders the home page of your application. /// Renders the home page of your application.
#[component] #[component]
pub fn AddTime() -> impl IntoView { pub fn AddTime() -> impl IntoView {
let participants = use_context::<RwSignal<Vec<schemas::Participant>>>(); let participants = use_context::<RwSignal<Vec<schemas::Participant>>>().unwrap();
let form_submit = create_server_action::<AddTime>(); let container_ref: NodeRef<html::Ul> = create_node_ref();
let name_input_ref: NodeRef<html::Input> = create_node_ref();
let event = create_rw_signal("lifesaver".to_string());
let name = create_rw_signal("".to_string());
let time = create_rw_signal(Time {
minutes: 0,
seconds: 0,
milliseconds: 0,
});
let selected_index = create_rw_signal::<usize>(0);
let participants_sorted =
create_memo(move |_| sort_participants(participants.get(), name.get()));
let add_time_action = create_action(|input: &(schemas::Participant, String, u32)| {
let input = input.to_owned();
async move { add_time(input.0, input.1, input.2).await }
});
let form_submit = move |ev: ev::SubmitEvent| {
ev.prevent_default();
let participant = &participants_sorted.get()[selected_index.get()];
add_time_action.dispatch((
participant.clone(),
String::from("lifesaver"),
time.get().as_milliseconds(),
));
name.set("".to_string());
time.set(Time {
minutes: 0,
seconds: 0,
milliseconds: 0,
});
let _ = name_input_ref.get().unwrap().focus();
};
let _ = use_event_listener(name_input_ref, keydown, move |evt| {
match evt.key().as_str() {
"ArrowDown" => selected_index.update(|x| {
let len = participants.get_untracked().len();
if *x != len {
*x += 1;
}
}),
"ArrowUp" => selected_index.update(|x| {
if *x != 0 {
*x -= 1;
}
}),
"Enter" => evt.prevent_default(),
_ => (),
}
let el: web_sys::Element = container_ref
.get_untracked()
.unwrap()
.children()
.item(selected_index.get_untracked().try_into().unwrap())
.unwrap();
el.scroll_into_view_with_scroll_into_view_options(
&ScrollIntoViewOptions::new().block(web_sys::ScrollLogicalPosition::Center),
);
});
view! { view! {
<h2>"Tijd toevoegen"</h2> <h2>"Tijd toevoegen"</h2>
<ActionForm action=form_submit> <form on:submit=form_submit>
<label>Onderdeel</label>
<select autocomplete="off"
on:change=move |ev| {
event.set(event_target_value(&ev))
}
>
<option value="lifesaver">"Lifesaver"</option>
<option value="hindernis">"Hindernis"</option>
<option value="popduiken">"Popduiken"</option>
</select>
<label>Naam</label> <label>Naam</label>
<input type="text" name="name" autocomplete="off" /> <div class="autocomplete">
<input type="text"
name="name"
autocomplete="off"
autofocus=true
node_ref=name_input_ref
on:input=move |ev| {
name.set(event_target_value(&ev));
selected_index.set(0);
}
prop:value=name
/>
<ul node_ref=container_ref tabindex=-1>
{move || participants_sorted.get().into_iter().enumerate().map(|(i, participant)| view! {
<li on:click=move |_| selected_index.set(i) class:selected=move || selected_index.get() == i>{participant.name + " " + "(" + &participant.group + ")" }</li>
}).collect_view()}
</ul>
</div>
<label>Tijd</label>
<div class="time">
<input type="number"
autocomplete="off"
placeholder="mm"
min=0
max=99
on:input=move |ev| {
time.update(|time| time.minutes = match event_target_value(&ev).parse::<u32>() {
Ok(x) => x,
Err(_) => 0,
});
}
prop:value=move || time.get().minutes
/>
<input type="number"
autocomplete="off"
placeholder="ss"
min=0
max=59
on:input=move |ev| {
time.update(|time| time.seconds = match event_target_value(&ev).parse::<u32>() {
Ok(x) => x,
Err(_) => 0,
});
}
prop:value=move || time.get().seconds
/>
<input type="number"
autocomplete="off"
placeholder="ms"
min=0
max=99
on:input=move |ev| {
time.update(|time| time.milliseconds = match event_target_value(&ev).parse::<u32>() {
Ok(x) => x,
Err(_) => 0,
});
}
prop:value=move || time.get().milliseconds
/>
</div>
<input type="submit" value="Tijd toevoegen" /> <input type="submit" value="Tijd toevoegen" />
</ActionForm> </form>
<p>{ move || format!("{:?}", participants.unwrap().get()) }</p>
} }
} }
#[component]
pub fn SelectOption(is: &'static str, value: ReadSignal<String>) -> impl IntoView {
view! {
<option
value=is
selected=move || value.get() == is
>
{is}
</option>
}
}
fn sort_participants(
participants: Vec<schemas::Participant>,
search: String,
) -> Vec<schemas::Participant> {
let mut filtered_sorted_list: Vec<(schemas::Participant, f64)> = participants
.into_iter()
.map(|participant| {
(
participant.clone(),
normalized_damerau_levenshtein(
&participant.name.to_lowercase(),
&search.to_lowercase(),
),
)
})
.collect();
filtered_sorted_list.sort_by(|a, b| {
let (_, sim_score_a) = a;
let (_, sim_score_b) = b;
sim_score_b
.partial_cmp(sim_score_a)
.unwrap_or(std::cmp::Ordering::Equal)
});
filtered_sorted_list
.into_iter()
.map(|(item, _)| item)
.collect()
}

View File

@ -6,11 +6,11 @@ cfg_if::cfg_if! {
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct Events { pub struct Events {
lifesaver: String, lifesaver: Option<u32>,
hindernis: String, hindernis: Option<u32>,
popduiken: String, popduiken: Option<u32>,
} }
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
@ -22,7 +22,7 @@ pub struct ParticipantRecord {
pub events: Option<Events>, pub events: Option<Events>,
} }
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct Participant { pub struct Participant {
pub id: String, pub id: String,
pub name: String, pub name: String,

View File

@ -12,6 +12,32 @@ form label {
margin-bottom: 3px; margin-bottom: 3px;
} }
.autocomplete {
display: flex;
flex-direction: column;
}
.autocomplete ul {
background-color: $secondary-bg-color-lighter;
list-style: none;
text-align: left;
padding: 5px;
margin-top: -15px;
max-height: 200px;
overflow-y: auto;
overflow-x: hidden;
border-radius: 5px;
}
.autocomplete ul .selected {
background-color: $secondary-color;
}
.autocomplete ul li {
padding: 3px 5px;
border-radius: 3px;
}
input,select { input,select {
background-color: $secondary-bg-color-light; background-color: $secondary-bg-color-light;
border: none; border: none;
@ -26,3 +52,21 @@ input[type=submit]:hover {
background-color: $secondary-bg-color-lighter; background-color: $secondary-bg-color-lighter;
cursor: pointer; cursor: pointer;
} }
form .time {
display: flex;
gap: 6px;
}
form .time input {
display: flex;
width: 30px;
text-align: center;
-moz-appearance: textfield;
}
form .time input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}