Added add time system
This commit is contained in:
parent
465f4a5673
commit
067a0860ab
@ -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",
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user