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. # It is not intended for manual editing.
version = 3 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]] [[package]]
name = "anstream" name = "anstream"
version = "0.3.2" version = "0.3.2"
@ -132,6 +147,7 @@ name = "backend"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"axum", "axum",
"chrono",
"clap", "clap",
"common", "common",
"futures-util", "futures-util",
@ -219,6 +235,22 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 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]] [[package]]
name = "clap" name = "clap"
version = "4.3.3" version = "4.3.3"
@ -271,6 +303,7 @@ checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
name = "common" name = "common"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"chrono",
"serde", "serde",
"thiserror", "thiserror",
"uuid", "uuid",
@ -286,6 +319,12 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "core-foundation-sys"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
[[package]] [[package]]
name = "cpufeatures" name = "cpufeatures"
version = "0.2.6" version = "0.2.6"
@ -355,6 +394,7 @@ dependencies = [
name = "frontend" name = "frontend"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"chrono",
"common", "common",
"futures", "futures",
"log", "log",
@ -362,6 +402,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"thiserror", "thiserror",
"uuid",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"wasm-logger", "wasm-logger",
@ -479,7 +520,7 @@ dependencies = [
"cfg-if", "cfg-if",
"js-sys", "js-sys",
"libc", "libc",
"wasi", "wasi 0.11.0+wasi-snapshot-preview1",
"wasm-bindgen", "wasm-bindgen",
] ]
@ -803,6 +844,29 @@ dependencies = [
"want", "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]] [[package]]
name = "idna" name = "idna"
version = "0.3.0" version = "0.3.0"
@ -939,7 +1003,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
dependencies = [ dependencies = [
"libc", "libc",
"wasi", "wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys", "windows-sys",
] ]
@ -953,6 +1017,15 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "num-traits"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
dependencies = [
"autocfg",
]
[[package]] [[package]]
name = "num_cpus" name = "num_cpus"
version = "1.15.0" version = "1.15.0"
@ -1389,6 +1462,17 @@ dependencies = [
"once_cell", "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]] [[package]]
name = "tinyvec" name = "tinyvec"
version = "1.6.0" version = "1.6.0"
@ -1700,6 +1784,12 @@ dependencies = [
"try-lock", "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]] [[package]]
name = "wasi" name = "wasi"
version = "0.11.0+wasi-snapshot-preview1" 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" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 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]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.48.0" version = "0.48.0"

View File

@ -9,4 +9,10 @@ edition = "2021"
[workspace.dependencies] [workspace.dependencies]
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
common = { path = "crates/common" } 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] [dependencies]
serde.workspace = true serde.workspace = true
uuid.workspace = true
common.workspace = true
thiserror.workspace = true
axum = { version = "0.6", features = ["ws", "headers"] } axum = { version = "0.6", features = ["ws", "headers"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
tracing = "0.1" tracing = "0.1"
tracing-subscriber = "0.3" tracing-subscriber = "0.3"
common.workspace = true tower = "0.4"
thiserror = "1.0.40" tower-http = { version = "0.4", features = ["fs", "trace", "cors"] }
tower = "0.4.13" chrono.workspace = true
uuid.workspace = true tokio-stream = { version = "0.1", features = ["sync"] }
tower-http = { version = "0.4.0", features = ["fs", "trace", "cors"] } clap = { version = "4", features = ["derive"] }
tokio-stream = { version = "0.1.14", features = ["sync"] } futures-util.workspace = true
clap = { version = "4.3.3", features = ["derive"] } serde_json.workspace = true
futures-util = "0.3.28"

View File

@ -20,6 +20,7 @@ use common::DeleteMilestone;
use common::Milestone; use common::Milestone;
use common::RestResponse; use common::RestResponse;
use common::ToggleAchievement; use common::ToggleAchievement;
use common::UpdateAchievementTimeOfReveal;
use futures_util::SinkExt; use futures_util::SinkExt;
use futures_util::StreamExt; use futures_util::StreamExt;
use serde::Deserialize; use serde::Deserialize;
@ -133,6 +134,10 @@ async fn main() {
.route("/api/v1/toggle", post(toggle_achievement)) .route("/api/v1/toggle", post(toggle_achievement))
.route("/api/v1/create-milestone", post(create_milestone)) .route("/api/v1/create-milestone", post(create_milestone))
.route("/api/v1/delete-milestone", post(delete_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)) .route("/api/ws", get(ws_handler))
.layer( .layer(
ServiceBuilder::new() ServiceBuilder::new()
@ -245,10 +250,12 @@ async fn create_achievement(
Json(create_achievement): Json<CreateAchievement>, Json(create_achievement): Json<CreateAchievement>,
) -> Response<()> { ) -> Response<()> {
tracing::debug!("Creating achievement: {create_achievement:?}."); tracing::debug!("Creating achievement: {create_achievement:?}.");
let CreateAchievement { goal, time_of_reveal } = create_achievement;
let achievement = Achievement { let achievement = Achievement {
goal: create_achievement.goal, goal,
completed: false, completed: false,
uuid: uuid::Uuid::new_v4(), uuid: uuid::Uuid::new_v4(),
time_of_reveal,
}; };
let mut lock = app_state.write().await; let mut lock = app_state.write().await;
lock.app_state.state.achievements.push(achievement); lock.app_state.state.achievements.push(achievement);
@ -326,6 +333,27 @@ async fn toggle_achievement(
Ok((StatusCode::OK, Json(Ok(())))) 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( async fn delete_achievement(
Extension(app_state): Extension<SharedState>, Extension(app_state): Extension<SharedState>,
Json(delete_achievement): Json<DeleteAchievement>, Json(delete_achievement): Json<DeleteAchievement>,

View File

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

View File

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

View File

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

View File

@ -23,6 +23,7 @@ pub fn AchievementComponent(props: &Props) -> Html {
goal, goal,
completed, completed,
uuid, uuid,
time_of_reveal: _,
} = &props.achievement; } = &props.achievement;
let uuid = *uuid; 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"); log::info!("Creating achievement");
let payload = CreateAchievement { let payload = CreateAchievement {
goal: self.input_value.clone(), goal: self.input_value.clone(),
time_of_reveal: None,
}; };
let link = ctx.link().clone(); let link = ctx.link().clone();
spawn_local(async move { spawn_local(async move {

View File

@ -1,4 +1,6 @@
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

@ -2,6 +2,7 @@ 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;
@ -27,6 +28,8 @@ enum Route {
CreateAchievement, CreateAchievement,
#[at("/create-milestone")] #[at("/create-milestone")]
CreateMilestone, CreateMilestone,
#[at("/reveal-times")]
RevealTimes,
#[not_found] #[not_found]
#[at("/404")] #[at("/404")]
NotFound, NotFound,
@ -39,6 +42,7 @@ 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/>},
} }
} }

View File

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