feat: set timed reveals

This commit is contained in:
Asger Juul Brunshøj 2023-06-16 21:14:53 +02:00
parent 17faadf7cd
commit 35d53d82c6
15 changed files with 350 additions and 25 deletions

103
Cargo.lock generated
View File

@ -2,6 +2,21 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "anstream"
version = "0.3.2"
@ -132,6 +147,7 @@ name = "backend"
version = "0.1.0"
dependencies = [
"axum",
"chrono",
"clap",
"common",
"futures-util",
@ -219,6 +235,22 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"time",
"wasm-bindgen",
"winapi",
]
[[package]]
name = "clap"
version = "4.3.3"
@ -271,6 +303,7 @@ checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
name = "common"
version = "0.1.0"
dependencies = [
"chrono",
"serde",
"thiserror",
"uuid",
@ -286,6 +319,12 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
[[package]]
name = "cpufeatures"
version = "0.2.6"
@ -355,6 +394,7 @@ dependencies = [
name = "frontend"
version = "0.1.0"
dependencies = [
"chrono",
"common",
"futures",
"log",
@ -362,6 +402,7 @@ dependencies = [
"serde",
"serde_json",
"thiserror",
"uuid",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-logger",
@ -479,7 +520,7 @@ dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasm-bindgen",
]
@ -803,6 +844,29 @@ dependencies = [
"want",
]
[[package]]
name = "iana-time-zone"
version = "0.1.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "idna"
version = "0.3.0"
@ -939,7 +1003,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
dependencies = [
"libc",
"wasi",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys",
]
@ -953,6 +1017,15 @@ dependencies = [
"winapi",
]
[[package]]
name = "num-traits"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
dependencies = [
"autocfg",
]
[[package]]
name = "num_cpus"
version = "1.15.0"
@ -1389,6 +1462,17 @@ dependencies = [
"once_cell",
]
[[package]]
name = "time"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a"
dependencies = [
"libc",
"wasi 0.10.0+wasi-snapshot-preview1",
"winapi",
]
[[package]]
name = "tinyvec"
version = "1.6.0"
@ -1700,6 +1784,12 @@ dependencies = [
"try-lock",
]
[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
@ -1817,6 +1907,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.48.0"

View File

@ -9,4 +9,10 @@ edition = "2021"
[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
common = { path = "crates/common" }
uuid = { vserion = "1.3", features = ["serde", "v4", "js"] }
uuid = { vserion = "1.3", features = ["serde", "v4"] }
chrono = { version = "0.4", features = ["serde"] }
thiserror = "1"
serde_json = "1"
log = "0.4"
futures = "0.3"
futures-util = "0.3"

View File

@ -6,16 +6,17 @@ edition.workspace = true
[dependencies]
serde.workspace = true
uuid.workspace = true
common.workspace = true
thiserror.workspace = true
axum = { version = "0.6", features = ["ws", "headers"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
tracing = "0.1"
tracing-subscriber = "0.3"
common.workspace = true
thiserror = "1.0.40"
tower = "0.4.13"
uuid.workspace = true
tower-http = { version = "0.4.0", features = ["fs", "trace", "cors"] }
tokio-stream = { version = "0.1.14", features = ["sync"] }
clap = { version = "4.3.3", features = ["derive"] }
futures-util = "0.3.28"
tower = "0.4"
tower-http = { version = "0.4", features = ["fs", "trace", "cors"] }
chrono.workspace = true
tokio-stream = { version = "0.1", features = ["sync"] }
clap = { version = "4", features = ["derive"] }
futures-util.workspace = true
serde_json.workspace = true

View File

@ -20,6 +20,7 @@ use common::DeleteMilestone;
use common::Milestone;
use common::RestResponse;
use common::ToggleAchievement;
use common::UpdateAchievementTimeOfReveal;
use futures_util::SinkExt;
use futures_util::StreamExt;
use serde::Deserialize;
@ -133,6 +134,10 @@ async fn main() {
.route("/api/v1/toggle", post(toggle_achievement))
.route("/api/v1/create-milestone", post(create_milestone))
.route("/api/v1/delete-milestone", post(delete_milestone))
.route(
"/api/v1/update-time-of-reveal",
post(update_achievement_time_of_reveal),
)
.route("/api/ws", get(ws_handler))
.layer(
ServiceBuilder::new()
@ -245,10 +250,12 @@ async fn create_achievement(
Json(create_achievement): Json<CreateAchievement>,
) -> Response<()> {
tracing::debug!("Creating achievement: {create_achievement:?}.");
let CreateAchievement { goal, time_of_reveal } = create_achievement;
let achievement = Achievement {
goal: create_achievement.goal,
goal,
completed: false,
uuid: uuid::Uuid::new_v4(),
time_of_reveal,
};
let mut lock = app_state.write().await;
lock.app_state.state.achievements.push(achievement);
@ -326,6 +333,27 @@ async fn toggle_achievement(
Ok((StatusCode::OK, Json(Ok(()))))
}
async fn update_achievement_time_of_reveal(
Extension(app_state): Extension<SharedState>,
Json(update): Json<UpdateAchievementTimeOfReveal>,
) -> Response<()> {
tracing::debug!("Updating achievement time of reveal: {update:?}.");
let mut lock = app_state.write().await;
if let Some(achievement) = lock
.app_state
.state
.achievements
.iter_mut()
.find(|x| x.uuid == update.uuid)
{
achievement.time_of_reveal = update.time_of_reveal;
lock.watcher_tx
.send(lock.app_state.state.clone())
.expect("watch channel is closed, every receiver was dropped.");
}
Ok((StatusCode::OK, Json(Ok(()))))
}
async fn delete_achievement(
Extension(app_state): Extension<SharedState>,
Json(delete_achievement): Json<DeleteAchievement>,

View File

@ -6,5 +6,6 @@ edition.workspace = true
[dependencies]
serde.workspace = true
thiserror = "1"
thiserror.workspace = true
uuid.workspace = true
chrono.workspace = true

View File

@ -48,11 +48,13 @@ pub struct Achievement {
pub goal: String,
pub completed: bool,
pub uuid: uuid::Uuid,
pub time_of_reveal: Option<chrono::NaiveTime>,
}
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
pub struct CreateAchievement {
pub goal: String,
pub time_of_reveal: Option<chrono::NaiveTime>,
}
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
@ -60,6 +62,12 @@ pub struct ToggleAchievement {
pub uuid: uuid::Uuid,
}
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
pub struct UpdateAchievementTimeOfReveal {
pub uuid: uuid::Uuid,
pub time_of_reveal: Option<chrono::NaiveTime>,
}
#[derive(PartialEq, Eq, Debug, Serialize, Clone, Deserialize)]
pub struct DeleteAchievement {
pub uuid: uuid::Uuid,

View File

@ -5,17 +5,19 @@ authors.workspace = true
edition.workspace = true
[dependencies]
log = "0.4.19"
wasm-logger = "0.2.0"
log.workspace = true
wasm-logger = "0.2"
yew = { version = "0.20", features = ["csr"] }
yew-router = "0.17.0"
yew-router = "0.17"
common.workspace = true
futures = "0.3.28"
wasm-bindgen-futures = "0.4.36"
reqwasm = "0.5.0"
yew-agent = "0.2.0"
futures.workspace = true
wasm-bindgen-futures = "0.4"
reqwasm = "0.5"
chrono.workspace = true
yew-agent = "0.2"
serde.workspace = true
serde_json = "1.0.96"
web-sys = "0.3.63"
wasm-bindgen = "0.2.86"
thiserror = "1.0.40"
serde_json.workspace = true
web-sys = "0.3"
wasm-bindgen = "0.2"
thiserror.workspace = true
uuid = { workspace = true, features = ["js"] }

View File

@ -23,6 +23,7 @@ pub fn AchievementComponent(props: &Props) -> Html {
goal,
completed,
uuid,
time_of_reveal: _,
} = &props.achievement;
let uuid = *uuid;

View File

@ -0,0 +1,130 @@
use crate::services::confirm::ConfirmService;
use crate::services::rest::RestService;
use common::Achievement;
use common::DeleteAchievement;
use common::ToggleAchievement;
use common::UpdateAchievementTimeOfReveal;
use std::ops::Deref;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::spawn_local;
use yew::classes;
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

@ -0,0 +1,32 @@
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

@ -51,6 +51,7 @@ impl Component for CreateAchievementComponent {
log::info!("Creating achievement");
let payload = CreateAchievement {
goal: self.input_value.clone(),
time_of_reveal: None,
};
let link = ctx.link().clone();
spawn_local(async move {

View File

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

View File

@ -2,6 +2,7 @@ use crate::components::error::error_component::ErrorComponent;
use crate::components::error::error_provider::ErrorProvider;
use crate::event_bus::EventBus;
use crate::services::websocket::WebsocketService;
use components::achievement_reveal_times::AchievementRevealTimes;
use components::admin::Admin;
use components::create_achievement::CreateAchievementComponent;
use components::create_milestone::CreateMilestoneComponent;
@ -27,6 +28,8 @@ enum Route {
CreateAchievement,
#[at("/create-milestone")]
CreateMilestone,
#[at("/reveal-times")]
RevealTimes,
#[not_found]
#[at("/404")]
NotFound,
@ -39,6 +42,7 @@ fn switch(selected_route: Route) -> Html {
Route::CreateAchievement => html! {<CreateAchievementComponent/>},
Route::CreateMilestone => html! {<CreateMilestoneComponent/>},
Route::NotFound => html! {<h1>{"404 not found"}</h1>},
Route::RevealTimes => html! {<AchievementRevealTimes/>},
}
}

View File

@ -4,6 +4,7 @@ use common::DeleteAchievement;
use common::DeleteMilestone;
use common::RestResponse;
use common::ToggleAchievement;
use common::UpdateAchievementTimeOfReveal;
use reqwasm::http::Request;
use serde::de::DeserializeOwned;
use serde::Serialize;
@ -67,4 +68,10 @@ impl RestService {
pub async fn delete_milestone(payload: DeleteMilestone) -> Result<(), RestServiceError> {
Self::post_json(payload, "/api/v1/delete-milestone").await
}
pub async fn update_time_of_reveal(
payload: UpdateAchievementTimeOfReveal,
) -> Result<(), RestServiceError> {
Self::post_json(payload, "/api/v1/update-time-of-reveal").await
}
}

3
todo.md Normal file
View File

@ -0,0 +1,3 @@
- UI errors from failed requests
- websocket reconnect
- disable timed reveal