milestones
This commit is contained in:
129
crates/frontend/src/components/create_milestone.rs
Normal file
129
crates/frontend/src/components/create_milestone.rs
Normal file
@@ -0,0 +1,129 @@
|
||||
use crate::services::rest::RestService;
|
||||
use common::CreateMilestone;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_futures::spawn_local;
|
||||
use web_sys::HtmlInputElement;
|
||||
use yew::html;
|
||||
use yew::Callback;
|
||||
use yew::Component;
|
||||
use yew::Html;
|
||||
use yew::NodeRef;
|
||||
use yew_router::scope_ext::RouterScopeExt;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Msg {
|
||||
Submit,
|
||||
UpdateInput(String),
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct CreateMilestoneComponent {
|
||||
input_value: String,
|
||||
submitted: bool,
|
||||
input_ref: NodeRef,
|
||||
}
|
||||
impl Component for CreateMilestoneComponent {
|
||||
type Message = Msg;
|
||||
type Properties = ();
|
||||
|
||||
fn create(_ctx: &yew::Context<Self>) -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn rendered(&mut self, _ctx: &yew::Context<Self>, first_render: bool) {
|
||||
if first_render {
|
||||
if let Some(input_element) = self.input_ref.get() {
|
||||
let input_element = input_element
|
||||
.dyn_into::<HtmlInputElement>()
|
||||
.expect("Failed to cast input element");
|
||||
input_element.focus().expect("Failed to focus input");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, _ctx: &yew::Context<Self>, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
Msg::Submit => {
|
||||
log::info!("Creating achievement");
|
||||
let Ok(goal) = self.input_value.parse::<f64>() else { return false; };
|
||||
|
||||
let goal = goal as usize;
|
||||
let payload = CreateMilestone { goal };
|
||||
spawn_local(async move {
|
||||
match RestService::create_milestone(payload).await {
|
||||
Ok(_response) => {}
|
||||
Err(_err) => {}
|
||||
}
|
||||
});
|
||||
self.submitted = true;
|
||||
|
||||
true
|
||||
}
|
||||
Msg::UpdateInput(value) => {
|
||||
self.input_value = value;
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &yew::Context<Self>) -> Html {
|
||||
let link = ctx.link().clone();
|
||||
let nav = ctx.link().navigator().unwrap();
|
||||
|
||||
if self.submitted {
|
||||
let onclick_go_back = Callback::from(move |_: web_sys::MouseEvent| {
|
||||
nav.push(&crate::Route::Root);
|
||||
});
|
||||
|
||||
html! {
|
||||
<>
|
||||
<div class="row" style="margin-top: 15%">
|
||||
<div class="twelve columns">
|
||||
<h4>{"Submitted!"}</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="six columns">
|
||||
<button onclick={onclick_go_back} class="button">{"Go back"}</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
} else {
|
||||
let onclick_go_back = Callback::from(move |_: web_sys::MouseEvent| {
|
||||
nav.push(&crate::Route::Root);
|
||||
});
|
||||
|
||||
let onsubmit = link.callback(|e: web_sys::SubmitEvent| {
|
||||
e.prevent_default();
|
||||
Msg::Submit
|
||||
});
|
||||
|
||||
let oninput = link.callback(|e: web_sys::InputEvent| {
|
||||
let Some(input) = e
|
||||
.target()
|
||||
.and_then(|t| t.dyn_into::<web_sys::HtmlInputElement>().ok()) else { unreachable!() };
|
||||
Msg::UpdateInput(input.value())
|
||||
});
|
||||
|
||||
html! {
|
||||
<>
|
||||
<div class="row">
|
||||
<button onclick={onclick_go_back} class="button">{"Back"}</button>
|
||||
</div>
|
||||
|
||||
<form {onsubmit} >
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="twelve columns">
|
||||
<label for="milestoneInput">{"New milestone"}</label>
|
||||
<input ref={self.input_ref.clone()} {oninput} value={self.input_value.to_string()} class="u-full-width" type="number" id="milestoneInput" />
|
||||
</div>
|
||||
</div>
|
||||
<input class="button-primary" type="submit" value="Submit" />
|
||||
</form>
|
||||
</>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
37
crates/frontend/src/components/milestone.rs
Normal file
37
crates/frontend/src/components/milestone.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use common::Milestone;
|
||||
use yew::prelude::*;
|
||||
|
||||
#[derive(Properties, Clone, PartialEq)]
|
||||
pub struct Props {
|
||||
pub milestone: Milestone,
|
||||
pub completed_achievements: usize,
|
||||
}
|
||||
|
||||
#[function_component(MilestoneComponent)]
|
||||
pub fn milestone_component(props: &Props) -> Html {
|
||||
let unfilled = props.milestone.goal - props.completed_achievements.min(props.milestone.goal);
|
||||
let filled = props.completed_achievements.min(props.milestone.goal);
|
||||
|
||||
let unfilled_stars = std::iter::repeat(html! {
|
||||
<i class={classes!("fas", "fa-fw", "fa-star-half-stroke")} />
|
||||
})
|
||||
.take(unfilled)
|
||||
.collect::<Html>();
|
||||
|
||||
let filled_stars = std::iter::repeat(html! {
|
||||
<i style="color: var(--secondary-color)" class={classes!("fas", "fa-fw", "fa-star")} />
|
||||
})
|
||||
.take(filled)
|
||||
.collect::<Html>();
|
||||
|
||||
html! {
|
||||
<div class="row">
|
||||
<p style="text-align: center" class="u-full-width">
|
||||
{format!("{} / {}", props.completed_achievements, props.milestone.goal)}
|
||||
<br />
|
||||
{filled_stars}
|
||||
{unfilled_stars}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
pub mod achievement;
|
||||
pub mod admin;
|
||||
pub mod create_achievement;
|
||||
pub mod create_milestone;
|
||||
pub mod milestone;
|
||||
pub mod root;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::components::achievement::AchievementComponent;
|
||||
use crate::components::milestone::MilestoneComponent;
|
||||
use crate::event_bus::EventBus;
|
||||
use crate::services::websocket::WebsocketService;
|
||||
use std::rc::Rc;
|
||||
@@ -51,8 +52,15 @@ impl Component for Root {
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
let nav = ctx.link().navigator().unwrap();
|
||||
|
||||
let onclick_create_achievement = Callback::from(move |_: web_sys::MouseEvent| {
|
||||
nav.push(&crate::Route::CreateAchievement);
|
||||
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 Some(state) = &self.state else {
|
||||
@@ -72,20 +80,41 @@ impl Component for Root {
|
||||
})
|
||||
.collect::<Html>();
|
||||
|
||||
let completed_achievements = state.achievements.iter().filter(|a| a.completed).count();
|
||||
|
||||
let mut milestones = state.milestones.clone();
|
||||
milestones.sort_by_key(|m| m.goal);
|
||||
let milestones = milestones.into_iter().map(|m| html! { <MilestoneComponent milestone={m} completed_achievements={completed_achievements} /> }).collect::<Html>();
|
||||
|
||||
html! {
|
||||
<>
|
||||
|
||||
<h1>{"Polter A"}<span style="color: var(--danger-color); text-decoration: line-through">{"bend"}</span>{"chievements!"}</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 achievement"}</button>
|
||||
<button onclick={onclick_create_achievement} class="button-primary">{"New"}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
{achievements}
|
||||
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use components::admin::Admin;
|
||||
use components::create_achievement::CreateAchievementComponent;
|
||||
use components::create_milestone::CreateMilestoneComponent;
|
||||
use components::root::Root;
|
||||
use std::rc::Rc;
|
||||
use yew::prelude::*;
|
||||
@@ -19,6 +20,8 @@ enum Route {
|
||||
Admin,
|
||||
#[at("/create-achievement")]
|
||||
CreateAchievement,
|
||||
#[at("/create-milestone")]
|
||||
CreateMilestone,
|
||||
#[not_found]
|
||||
#[at("/404")]
|
||||
NotFound,
|
||||
@@ -29,6 +32,7 @@ fn switch(selected_route: Route) -> Html {
|
||||
Route::Root => html! {<Root />},
|
||||
Route::Admin => html! {<Admin/>},
|
||||
Route::CreateAchievement => html! {<CreateAchievementComponent/>},
|
||||
Route::CreateMilestone => html! {<CreateMilestoneComponent/>},
|
||||
Route::NotFound => html! {<h1>{"404 not found"}</h1>},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use common::CreateAchievement;
|
||||
use common::CreateMilestone;
|
||||
use common::DeleteAchievement;
|
||||
use common::DeleteMilestone;
|
||||
use common::ToggleAchievement;
|
||||
use reqwasm::http::Request;
|
||||
use serde::Serialize;
|
||||
@@ -58,11 +60,19 @@ impl RestService {
|
||||
Self::post_json(payload, "/toggle").await
|
||||
}
|
||||
|
||||
pub async fn create_achievement(payload: CreateAchievement) -> Result<(), RestServiceError> {
|
||||
Self::post_json(payload, "/create").await
|
||||
}
|
||||
|
||||
pub async fn delete_achievement(payload: DeleteAchievement) -> Result<(), RestServiceError> {
|
||||
Self::post_json(payload, "/delete").await
|
||||
}
|
||||
|
||||
pub async fn create_achievement(payload: CreateAchievement) -> Result<(), RestServiceError> {
|
||||
Self::post_json(payload, "/create").await
|
||||
pub async fn create_milestone(payload: CreateMilestone) -> Result<(), RestServiceError> {
|
||||
Self::post_json(payload, "/create-milestone").await
|
||||
}
|
||||
|
||||
pub async fn delete_milestone(payload: DeleteMilestone) -> Result<(), RestServiceError> {
|
||||
Self::post_json(payload, "/delete-milestone").await
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user