admin view

This commit is contained in:
Asger Juul Brunshøj 2023-06-19 10:53:14 +02:00
parent 05139b19bc
commit 5b49586da3
8 changed files with 253 additions and 231 deletions

View File

@ -1,7 +1,5 @@
use crate::services::confirm::ConfirmService;
use crate::services::rest::RestService; use crate::services::rest::RestService;
use common::Achievement; use common::Achievement;
use common::DeleteAchievement;
use common::ToggleAchievement; use common::ToggleAchievement;
use wasm_bindgen_futures::spawn_local; use wasm_bindgen_futures::spawn_local;
use yew::classes; use yew::classes;
@ -38,20 +36,6 @@ pub fn AchievementComponent(props: &Props) -> Html {
}); });
}); });
let onclick_delete = Callback::from(move |_| {
if !ConfirmService::confirm("Are you sure you want to delete?") {
return;
}
log::info!("Delete achievement confirmed.");
spawn_local(async move {
match RestService::delete_achievement(DeleteAchievement { uuid }).await {
Ok(_response) => {}
Err(_err) => {}
}
});
});
let toggle_button_class = if *completed { let toggle_button_class = if *completed {
"button-primary color-secondary" "button-primary color-secondary"
} else { } else {
@ -69,9 +53,6 @@ pub fn AchievementComponent(props: &Props) -> Html {
<div class="flex-grow"> <div class="flex-grow">
<p>{goal}</p> <p>{goal}</p>
</div> </div>
<div class="flex-intrinsic-size">
<button onclick={onclick_delete} class="button narrow color-danger"><i class="fas fa-trash"/></button>
</div>
</div> </div>
} }
} }

View File

@ -1,125 +0,0 @@
use crate::services::rest::RestService;
use common::Achievement;
use common::UpdateAchievementTimeOfReveal;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::spawn_local;
use yew::function_component;
use yew::html;
use yew::use_state;
use yew::Callback;
use yew::Html;
use yew::Properties;
#[derive(Properties, PartialEq)]
pub struct Props {
pub achievement: Achievement,
pub number: usize,
}
#[function_component]
pub fn AchievementRevealTime(props: &Props) -> Html {
let achievement = &props.achievement;
let uuid = achievement.uuid;
let time_of_reveal: Option<String> = achievement
.time_of_reveal
.map(|naive_time| naive_time.format("%H:%M").to_string());
let timed_reveal_enabled = use_state(|| time_of_reveal.is_some());
let input_time = use_state(|| time_of_reveal.clone().unwrap_or("".to_string()));
let awaiting_response = use_state(|| false);
let onsubmit = {
let awaiting_response = awaiting_response.clone();
let timed_reveal_enabled = timed_reveal_enabled.clone();
let input_time = input_time.clone();
Callback::from(move |e: web_sys::SubmitEvent| {
e.prevent_default();
let new_time_of_reveal = if *timed_reveal_enabled {
if let Ok(naive_time) = chrono::NaiveTime::parse_from_str(&input_time, "%H:%M") {
Some(naive_time)
} else {
// TODO: show UI error
log::debug!("Could not parse time: {}", *input_time);
return;
}
} else {
None
};
let payload = UpdateAchievementTimeOfReveal {
time_of_reveal: new_time_of_reveal,
uuid,
};
awaiting_response.set(true);
let awaiting_response = awaiting_response.clone();
spawn_local(async move {
let res = RestService::update_time_of_reveal(payload).await;
awaiting_response.set(false);
});
})
};
let oninput_time = {
let input_time = input_time.clone();
Callback::from(move |e: web_sys::InputEvent| {
let Some(input) = e
.target()
.and_then(|t| t.dyn_into::<web_sys::HtmlInputElement>().ok()) else { unreachable!() };
log::debug!("{:?}", input.value());
input_time.set(input.value());
})
};
let oninput_timed_reveal_checkbox = {
let timed_reveal_enabled = timed_reveal_enabled.clone();
Callback::from(move |e: web_sys::InputEvent| {
let Some(input) = e
.target()
.and_then(|t| t.dyn_into::<web_sys::HtmlInputElement>().ok()) else { unreachable!() };
timed_reveal_enabled.set(input.checked());
})
};
let new_value: Option<&str> = timed_reveal_enabled.then_some(&**input_time);
let show_submit_button: bool = time_of_reveal.as_deref() != new_value;
html! {
<form {onsubmit}>
<div class="row flex">
// Achievement number
<div class="flex-intrinsic-size">
<p>{format!("{}.", props.number)}</p>
</div>
// Achievement text
<div class="flex-grow">
<p>{&achievement.goal}</p>
</div>
</div>
// Enable timed reveal checkbox
<label class="row">
<input oninput={oninput_timed_reveal_checkbox} checked={*timed_reveal_enabled} type="checkbox" />
<span class="label-body">{"Timed reveal"}</span>
</label>
{ if *timed_reveal_enabled { html! {
// Time input
<div>
<label for="revealTimeInput">{"Reveal time"}</label>
<input oninput={oninput_time} value={(*input_time).clone()} type="time" id="revealTimeInput" />
</div>
}} else { html! {}}}
// Submit button
{ if show_submit_button { html! {
<input class="button-primary" type="submit" value="Submit" disabled={ *awaiting_response } />
}} else { html! {}}}
<hr />
</form>
}
}

View File

@ -1,32 +0,0 @@
use crate::components::achievement_reveal_time::AchievementRevealTime;
use yew::functional::*;
use yew::prelude::*;
use yew_router::prelude::*;
#[function_component]
pub fn AchievementRevealTimes() -> Html {
let nav = use_navigator().expect("cannot get navigator");
let app_state = use_context::<crate::AppState>().expect("no app state ctx found");
let achievements = app_state
.state
.achievements
.iter()
.cloned()
.enumerate()
.map(|(idx, a)| (idx + 1, a))
.map(|(n, a)| {
html! {
<AchievementRevealTime number={n} achievement={a} />
}
})
.collect::<Html>();
html! {
<>
<h1>{"Achievement Timed Reveals"}</h1>
<hr />
{achievements}
</>
}
}

View File

@ -1,7 +1,252 @@
use crate::services::confirm::ConfirmService;
use crate::services::rest::RestService;
use common::DeleteAchievement;
use common::DeleteMilestone;
use common::UpdateAchievementTimeOfReveal;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::spawn_local;
use yew::function_component;
use yew::functional::*; use yew::functional::*;
use yew::prelude::*; use yew::html;
use yew::use_state;
use yew::Callback;
use yew::Html;
use yew::Properties;
use yew_router::prelude::use_navigator;
#[function_component(Admin)] #[function_component]
pub fn admin() -> Html { pub fn Admin() -> Html {
html! {} let nav = use_navigator().expect("cannot get navigator");
let app_state = use_context::<crate::AppState>().expect("no app state ctx found");
let achievements = app_state
.state
.achievements
.iter()
.cloned()
.enumerate()
.map(|(idx, a)| (idx + 1, a))
.map(|(n, a)| {
html! {
<Achievement number={n} achievement={a} />
}
})
.collect::<Html>();
let onclick_create_achievement = {
let nav = nav.clone();
Callback::from(move |_: web_sys::MouseEvent| {
nav.push(&crate::Route::CreateAchievement);
})
};
let onclick_create_milestone = Callback::from(move |_: web_sys::MouseEvent| {
nav.push(&crate::Route::CreateMilestone);
});
let mut milestones = app_state.state.milestones.clone();
milestones.sort_by_key(|m| m.goal);
let milestones = milestones
.into_iter()
.map(|m| html! { <Milestone milestone={m}/> })
.collect::<Html>();
html! {
<>
<h1>{"Admin View"}</h1>
<hr />
<div class="row flex flex-wrap">
<div class="flex-grow">
<h3>{"Milestones"}</h3>
</div>
<div class="flex-intrinsic-size">
<button onclick={onclick_create_milestone} class="button-primary">{"New"}</button>
</div>
</div>
{milestones}
<hr />
<div class="row flex flex-wrap">
<div class="flex-grow">
<h3>{"Achievements"}</h3>
</div>
<div class="flex-intrinsic-size">
<button onclick={onclick_create_achievement} class="button-primary">{"New"}</button>
</div>
</div>
{achievements}
</>
}
}
#[derive(Properties, PartialEq)]
struct AchievementProps {
achievement: common::Achievement,
number: usize,
}
#[function_component]
fn Achievement(props: &AchievementProps) -> Html {
let achievement = &props.achievement;
let uuid = achievement.uuid;
let time_of_reveal: Option<String> = achievement
.time_of_reveal
.map(|naive_time| naive_time.format("%H:%M").to_string());
let timed_reveal_enabled = use_state(|| time_of_reveal.is_some());
let input_time = use_state(|| time_of_reveal.clone().unwrap_or("".to_string()));
let awaiting_response = use_state(|| false);
let onsubmit_timed_reveal = {
let awaiting_response = awaiting_response.clone();
let timed_reveal_enabled = timed_reveal_enabled.clone();
let input_time = input_time.clone();
Callback::from(move |e: web_sys::SubmitEvent| {
e.prevent_default();
let new_time_of_reveal = if *timed_reveal_enabled {
if let Ok(naive_time) = chrono::NaiveTime::parse_from_str(&input_time, "%H:%M") {
Some(naive_time)
} else {
// TODO: show UI error
log::debug!("Could not parse time: {}", *input_time);
return;
}
} else {
None
};
let payload = UpdateAchievementTimeOfReveal {
time_of_reveal: new_time_of_reveal,
uuid,
};
awaiting_response.set(true);
let awaiting_response = awaiting_response.clone();
spawn_local(async move {
let res = RestService::update_time_of_reveal(payload).await;
awaiting_response.set(false);
});
})
};
let oninput_time = {
let input_time = input_time.clone();
Callback::from(move |e: web_sys::InputEvent| {
let Some(input) = e
.target()
.and_then(|t| t.dyn_into::<web_sys::HtmlInputElement>().ok()) else { unreachable!() };
log::debug!("{:?}", input.value());
input_time.set(input.value());
})
};
let oninput_timed_reveal_checkbox = {
let timed_reveal_enabled = timed_reveal_enabled.clone();
Callback::from(move |e: web_sys::InputEvent| {
let Some(input) = e
.target()
.and_then(|t| t.dyn_into::<web_sys::HtmlInputElement>().ok()) else { unreachable!() };
timed_reveal_enabled.set(input.checked());
})
};
let onclick_delete = Callback::from(move |_| {
if !ConfirmService::confirm("Are you sure you want to delete?") {
return;
}
log::info!("Delete achievement confirmed.");
spawn_local(async move {
match RestService::delete_achievement(DeleteAchievement { uuid }).await {
Ok(_response) => {}
Err(_err) => {}
}
});
});
let new_value: Option<&str> = timed_reveal_enabled.then_some(&**input_time);
let show_submit_button: bool = time_of_reveal.as_deref() != new_value;
html! {
<div>
<div class="row flex">
// Achievement number
<div class="flex-intrinsic-size">
<p>{format!("{}.", props.number)}</p>
</div>
// Achievement text
<div class="flex-grow">
<p>{&achievement.goal}</p>
</div>
// Delete button
<button onclick={onclick_delete} class="flex-intrinsic-size button narrow color-danger"><i class="fas fa-trash" style="padding-right: 0.5em"/>{"Delete"}</button>
</div>
// Timed reveal form
<form onsubmit={onsubmit_timed_reveal}>
// Timed reveal: Enable checkbox
<label>
<span class="label-body" style="margin-right: 0.5rem; font-weight: bold"><i class="fas fa-clock" style="padding-right: 0.5em"/>{"Enable Timed Reveal:"}</span>
<input oninput={oninput_timed_reveal_checkbox} checked={*timed_reveal_enabled} type="checkbox" />
</label>
<div class="row flex">
// Time input
{ if *timed_reveal_enabled { html! {
<div>
<label for="revealTimeInput">{"Reveal time"}</label>
<input oninput={oninput_time} value={(*input_time).clone()} type="time" id="revealTimeInput" />
</div>
}} else { html! {}}}
// Timed reveal form submit button
{ if show_submit_button { html! {
<input class="button-primary" type="submit" value="Update" disabled={ *awaiting_response } />
}} else { html! {}}}
</div>
</form>
<hr />
</div>
}
}
#[function_component]
fn Milestone(props: &MilestoneProps) -> Html {
let uuid = props.milestone.uuid;
let onclick_delete = Callback::from(move |_| {
if !ConfirmService::confirm("Are you sure you want to delete?") {
return;
}
spawn_local(async move {
match RestService::delete_milestone(DeleteMilestone { uuid }).await {
Ok(_response) => {}
Err(_err) => {}
}
});
});
html! {
<div class="row flex">
<p class="flex-grow">{format!("Goal: {} achievements", props.milestone.goal)}</p>
<div class="flex-intrinsic-size">
<button onclick={onclick_delete} class="button narrow color-danger"><i style="margin-right: 0.5em" class="fas fa-trash"/>{"Delete"}</button>
</div>
</div>
}
}
#[derive(Properties, Clone, PartialEq)]
struct MilestoneProps {
milestone: common::Milestone,
} }

View File

@ -1,8 +1,4 @@
use crate::services::confirm::ConfirmService;
use crate::services::rest::RestService;
use common::DeleteMilestone;
use common::Milestone; use common::Milestone;
use wasm_bindgen_futures::spawn_local;
use yew::prelude::*; use yew::prelude::*;
#[derive(Properties, Clone, PartialEq)] #[derive(Properties, Clone, PartialEq)]
@ -28,21 +24,6 @@ pub fn milestone_component(props: &Props) -> Html {
.take(filled) .take(filled)
.collect::<Html>(); .collect::<Html>();
let uuid = props.milestone.uuid;
let onclick_delete = Callback::from(move |_| {
if !ConfirmService::confirm("Are you sure you want to delete?") {
return;
}
log::info!("Delete achievement confirmed.");
spawn_local(async move {
match RestService::delete_milestone(DeleteMilestone { uuid }).await {
Ok(_response) => {}
Err(_err) => {}
}
});
});
html! { html! {
<div class="row flex"> <div class="row flex">
<p style="text-align: center" class="flex-grow"> <p style="text-align: center" class="flex-grow">
@ -73,9 +54,6 @@ pub fn milestone_component(props: &Props) -> Html {
{filled_stars} {filled_stars}
{unfilled_stars} {unfilled_stars}
</p> </p>
<div class="flex-intrinsic-size">
<button onclick={onclick_delete} class="button narrow color-danger"><i class="fas fa-trash"/></button>
</div>
</div> </div>
} }
} }

View File

@ -1,6 +1,4 @@
pub mod achievement; pub mod achievement;
pub mod achievement_reveal_time;
pub mod achievement_reveal_times;
pub mod admin; pub mod admin;
pub mod create_achievement; pub mod create_achievement;
pub mod create_milestone; pub mod create_milestone;

View File

@ -10,17 +10,6 @@ pub fn Root() -> Html {
let nav = use_navigator().expect("cannot get navigator"); let nav = use_navigator().expect("cannot get navigator");
let app_state = use_context::<crate::AppState>().expect("no app state ctx found"); let app_state = use_context::<crate::AppState>().expect("no app state ctx found");
let onclick_create_achievement = {
let nav = nav.clone();
Callback::from(move |_: web_sys::MouseEvent| {
nav.push(&crate::Route::CreateAchievement);
})
};
let onclick_create_milestone = Callback::from(move |_: web_sys::MouseEvent| {
nav.push(&crate::Route::CreateMilestone);
});
let current_time: chrono::NaiveTime = chrono::Local::now().time(); let current_time: chrono::NaiveTime = chrono::Local::now().time();
let achievements = app_state.state.achievements.clone(); let achievements = app_state.state.achievements.clone();
@ -76,11 +65,7 @@ pub fn Root() -> Html {
<hr /> <hr />
<div class="row flex flex-wrap"> <div class="row flex flex-wrap">
<div class="flex-grow"> <div class="flex-grow">
<h3>{"Milestones"}</h3> <h3><i style="margin-right: 0.5em" class="fas fa-trophy" />{"Milestones"}</h3>
</div>
<div class="flex-intrinsic-size">
<button onclick={onclick_create_milestone} class="button-primary">{"New"}</button>
</div> </div>
</div> </div>
{milestones} {milestones}
@ -88,11 +73,7 @@ pub fn Root() -> Html {
<hr /> <hr />
<div class="row flex flex-wrap"> <div class="row flex flex-wrap">
<div class="flex-grow"> <div class="flex-grow">
<h3>{"Achievements"}</h3> <h3><i style="margin-right: 0.5em" class="fas fa-star" />{"Achievements"}</h3>
</div>
<div class="flex-intrinsic-size">
<button onclick={onclick_create_achievement} class="button-primary">{"New"}</button>
</div> </div>
</div> </div>
{achievements} {achievements}

View File

@ -2,7 +2,6 @@ use crate::components::error::error_component::ErrorComponent;
use crate::components::error::error_provider::ErrorProvider; use crate::components::error::error_provider::ErrorProvider;
use crate::event_bus::EventBus; use crate::event_bus::EventBus;
use crate::services::websocket::WebsocketService; use crate::services::websocket::WebsocketService;
use components::achievement_reveal_times::AchievementRevealTimes;
use components::admin::Admin; use components::admin::Admin;
use components::create_achievement::CreateAchievementComponent; use components::create_achievement::CreateAchievementComponent;
use components::create_milestone::CreateMilestoneComponent; use components::create_milestone::CreateMilestoneComponent;
@ -23,14 +22,12 @@ pub mod util;
enum Route { enum Route {
#[at("/")] #[at("/")]
Root, Root,
#[at("/chat")]
Admin,
#[at("/create-achievement")] #[at("/create-achievement")]
CreateAchievement, CreateAchievement,
#[at("/create-milestone")] #[at("/create-milestone")]
CreateMilestone, CreateMilestone,
#[at("/reveal-times")] #[at("/en-lille-nisse-rejste")]
RevealTimes, Admin,
#[not_found] #[not_found]
#[at("/404")] #[at("/404")]
NotFound, NotFound,
@ -43,7 +40,6 @@ fn switch(selected_route: Route) -> Html {
Route::CreateAchievement => html! {<CreateAchievementComponent/>}, Route::CreateAchievement => html! {<CreateAchievementComponent/>},
Route::CreateMilestone => html! {<CreateMilestoneComponent/>}, Route::CreateMilestone => html! {<CreateMilestoneComponent/>},
Route::NotFound => html! {<h1>{"404 not found"}</h1>}, Route::NotFound => html! {<h1>{"404 not found"}</h1>},
Route::RevealTimes => html! {<AchievementRevealTimes/>},
} }
} }