persistent websocket
This commit is contained in:
parent
70d99b0c6b
commit
78999174ab
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -167,6 +167,7 @@ name = "common"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
|
"thiserror",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ use common::CreateMilestone;
|
|||||||
use common::DeleteAchievement;
|
use common::DeleteAchievement;
|
||||||
use common::DeleteMilestone;
|
use common::DeleteMilestone;
|
||||||
use common::Milestone;
|
use common::Milestone;
|
||||||
|
use common::RestResponse;
|
||||||
use common::ToggleAchievement;
|
use common::ToggleAchievement;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
@ -37,6 +38,20 @@ struct SharedStateParts {
|
|||||||
watcher_tx: tokio::sync::watch::Sender<common::State>,
|
watcher_tx: tokio::sync::watch::Sender<common::State>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Response<T> = Result<(StatusCode, Json<RestResponse<T>>), HandlerError>;
|
||||||
|
// TODO: still needed?
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
enum HandlerError {
|
||||||
|
// #[error("Failed to lock state")]
|
||||||
|
// LockAppStateError,
|
||||||
|
}
|
||||||
|
impl IntoResponse for HandlerError {
|
||||||
|
fn into_response(self) -> axum::response::Response {
|
||||||
|
let error_message = format!("{self}");
|
||||||
|
(StatusCode::INTERNAL_SERVER_ERROR, error_message).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
@ -168,7 +183,7 @@ async fn handle_socket(mut socket: WebSocket, state_watch_rx: tokio::sync::watch
|
|||||||
async fn create_achievement(
|
async fn create_achievement(
|
||||||
Extension(app_state): Extension<SharedState>,
|
Extension(app_state): Extension<SharedState>,
|
||||||
Json(create_achievement): Json<CreateAchievement>,
|
Json(create_achievement): Json<CreateAchievement>,
|
||||||
) -> Result<(StatusCode, ()), HandlerError> {
|
) -> Response<()> {
|
||||||
tracing::debug!("Creating achievement: {create_achievement:?}.");
|
tracing::debug!("Creating achievement: {create_achievement:?}.");
|
||||||
let achievement = Achievement {
|
let achievement = Achievement {
|
||||||
goal: create_achievement.goal,
|
goal: create_achievement.goal,
|
||||||
@ -180,17 +195,21 @@ async fn create_achievement(
|
|||||||
lock.watcher_tx
|
lock.watcher_tx
|
||||||
.send(lock.app_state.state.clone())
|
.send(lock.app_state.state.clone())
|
||||||
.expect("watch channel is closed, every receiver was dropped.");
|
.expect("watch channel is closed, every receiver was dropped.");
|
||||||
Ok((StatusCode::CREATED, ()))
|
Ok((StatusCode::CREATED, Json(Ok(()))))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_milestone(
|
async fn create_milestone(
|
||||||
Extension(app_state): Extension<SharedState>,
|
Extension(app_state): Extension<SharedState>,
|
||||||
Json(create_milestone): Json<CreateMilestone>,
|
Json(create_milestone): Json<CreateMilestone>,
|
||||||
) -> Result<(StatusCode, ()), HandlerError> {
|
) -> Response<()> {
|
||||||
tracing::debug!("Creating milestone: {create_milestone:?}.");
|
tracing::debug!("Creating milestone: {create_milestone:?}.");
|
||||||
|
|
||||||
if create_milestone.goal > 100 {
|
let goal_max = 100;
|
||||||
return Ok((StatusCode::BAD_REQUEST, ()));
|
if create_milestone.goal > goal_max {
|
||||||
|
return Ok((
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
Json(Err(format!("Max goal allowed: {goal_max}.").into())),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let milestone = Milestone {
|
let milestone = Milestone {
|
||||||
@ -202,13 +221,13 @@ async fn create_milestone(
|
|||||||
lock.watcher_tx
|
lock.watcher_tx
|
||||||
.send(lock.app_state.state.clone())
|
.send(lock.app_state.state.clone())
|
||||||
.expect("watch channel is closed, every receiver was dropped.");
|
.expect("watch channel is closed, every receiver was dropped.");
|
||||||
Ok((StatusCode::CREATED, ()))
|
Ok((StatusCode::CREATED, Json(Ok(()))))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn delete_milestone(
|
async fn delete_milestone(
|
||||||
Extension(app_state): Extension<SharedState>,
|
Extension(app_state): Extension<SharedState>,
|
||||||
Json(delete_milestone): Json<DeleteMilestone>,
|
Json(delete_milestone): Json<DeleteMilestone>,
|
||||||
) -> Result<(StatusCode, ()), HandlerError> {
|
) -> Response<()> {
|
||||||
tracing::debug!("Deleting milestone: {delete_milestone:?}.");
|
tracing::debug!("Deleting milestone: {delete_milestone:?}.");
|
||||||
let mut lock = app_state.write().await;
|
let mut lock = app_state.write().await;
|
||||||
if let Some(pos) = lock
|
if let Some(pos) = lock
|
||||||
@ -223,13 +242,13 @@ async fn delete_milestone(
|
|||||||
.send(lock.app_state.state.clone())
|
.send(lock.app_state.state.clone())
|
||||||
.expect("watch channel is closed, every receiver was dropped.");
|
.expect("watch channel is closed, every receiver was dropped.");
|
||||||
}
|
}
|
||||||
Ok((StatusCode::OK, ()))
|
Ok((StatusCode::OK, Json(Ok(()))))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn toggle_achievement(
|
async fn toggle_achievement(
|
||||||
Extension(app_state): Extension<SharedState>,
|
Extension(app_state): Extension<SharedState>,
|
||||||
Json(toggle_achievement): Json<ToggleAchievement>,
|
Json(toggle_achievement): Json<ToggleAchievement>,
|
||||||
) -> Result<(StatusCode, ()), HandlerError> {
|
) -> Response<()> {
|
||||||
tracing::debug!("Toggling achievement: {toggle_achievement:?}.");
|
tracing::debug!("Toggling achievement: {toggle_achievement:?}.");
|
||||||
let mut lock = app_state.write().await;
|
let mut lock = app_state.write().await;
|
||||||
if let Some(achievement) = lock
|
if let Some(achievement) = lock
|
||||||
@ -244,13 +263,13 @@ async fn toggle_achievement(
|
|||||||
.send(lock.app_state.state.clone())
|
.send(lock.app_state.state.clone())
|
||||||
.expect("watch channel is closed, every receiver was dropped.");
|
.expect("watch channel is closed, every receiver was dropped.");
|
||||||
}
|
}
|
||||||
Ok((StatusCode::OK, ()))
|
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>,
|
||||||
) -> Result<(StatusCode, ()), HandlerError> {
|
) -> Response<()> {
|
||||||
tracing::debug!("Deleting achievement: {delete_achievement:?}.");
|
tracing::debug!("Deleting achievement: {delete_achievement:?}.");
|
||||||
let mut lock = app_state.write().await;
|
let mut lock = app_state.write().await;
|
||||||
if let Some(pos) = lock
|
if let Some(pos) = lock
|
||||||
@ -265,7 +284,7 @@ async fn delete_achievement(
|
|||||||
.send(lock.app_state.state.clone())
|
.send(lock.app_state.state.clone())
|
||||||
.expect("watch channel is closed, every receiver was dropped.");
|
.expect("watch channel is closed, every receiver was dropped.");
|
||||||
}
|
}
|
||||||
Ok((StatusCode::OK, ()))
|
Ok((StatusCode::OK, Json(Ok(()))))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn shutdown_signal() {
|
async fn shutdown_signal() {
|
||||||
@ -335,17 +354,3 @@ pub enum AppStateWriteError {
|
|||||||
#[error("Failed to serialize the state")]
|
#[error("Failed to serialize the state")]
|
||||||
SerializationError(#[from] serde_json::Error),
|
SerializationError(#[from] serde_json::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: still needed?
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
enum HandlerError {
|
|
||||||
// #[error("Failed to lock state")]
|
|
||||||
// LockAppStateError,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoResponse for HandlerError {
|
|
||||||
fn into_response(self) -> axum::response::Response {
|
|
||||||
let error_message = format!("{self}");
|
|
||||||
(StatusCode::INTERNAL_SERVER_ERROR, error_message).into_response()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -6,4 +6,5 @@ edition.workspace = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
thiserror = "1"
|
||||||
uuid.workspace = true
|
uuid.workspace = true
|
||||||
|
@ -2,6 +2,26 @@ use serde::Deserialize;
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
pub type WebSocketMessage = State;
|
pub type WebSocketMessage = State;
|
||||||
|
pub type RestResponse<T> = Result<T, RestError>;
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Default, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[error("{error}")]
|
||||||
|
pub struct RestError {
|
||||||
|
pub error: String,
|
||||||
|
}
|
||||||
|
impl RestError {
|
||||||
|
pub fn new(err: impl Into<String>) -> Self {
|
||||||
|
Self { error: err.into() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<T> From<T> for RestError
|
||||||
|
where
|
||||||
|
T: Into<String>,
|
||||||
|
{
|
||||||
|
fn from(value: T) -> Self {
|
||||||
|
RestError::new(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub struct State {
|
pub struct State {
|
||||||
|
@ -23,8 +23,10 @@
|
|||||||
<link data-trunk rel="rust" href="Cargo.toml" data-bin="app" data-type="main" />
|
<link data-trunk rel="rust" href="Cargo.toml" data-bin="app" data-type="main" />
|
||||||
<link data-trunk rel="rust" href="Cargo.toml" data-bin="event_bus" data-type="worker" />
|
<link data-trunk rel="rust" href="Cargo.toml" data-bin="event_bus" data-type="worker" />
|
||||||
</head>
|
</head>
|
||||||
|
<!--
|
||||||
<body>
|
<body>
|
||||||
<script src="//cdn.jsdelivr.net/npm/eruda"></script>
|
<script src="//cdn.jsdelivr.net/npm/eruda"></script>
|
||||||
<script>eruda.init();</script>
|
<script>eruda.init();</script>
|
||||||
</body>
|
</body>
|
||||||
|
-->
|
||||||
</html>
|
</html>
|
||||||
|
@ -2,5 +2,5 @@ use frontend::event_bus::EventBus;
|
|||||||
use yew_agent::PublicWorker;
|
use yew_agent::PublicWorker;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
EventBus::register();
|
EventBus::<common::WebSocketMessage>::register();
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,6 @@ use wasm_bindgen_futures::spawn_local;
|
|||||||
use yew::classes;
|
use yew::classes;
|
||||||
use yew::function_component;
|
use yew::function_component;
|
||||||
use yew::html;
|
use yew::html;
|
||||||
use yew::use_memo;
|
|
||||||
use yew::Callback;
|
use yew::Callback;
|
||||||
use yew::Html;
|
use yew::Html;
|
||||||
use yew::Properties;
|
use yew::Properties;
|
||||||
@ -25,8 +24,6 @@ pub fn AchievementComponent(props: &Props) -> Html {
|
|||||||
uuid,
|
uuid,
|
||||||
} = &props.achievement;
|
} = &props.achievement;
|
||||||
|
|
||||||
let confirm_service = use_memo(|_| ConfirmService, ());
|
|
||||||
|
|
||||||
let uuid = *uuid;
|
let uuid = *uuid;
|
||||||
|
|
||||||
let onclick_toggle = Callback::from(move |_| {
|
let onclick_toggle = Callback::from(move |_| {
|
||||||
@ -40,7 +37,7 @@ pub fn AchievementComponent(props: &Props) -> Html {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let onclick_delete = Callback::from(move |_| {
|
let onclick_delete = Callback::from(move |_| {
|
||||||
if !confirm_service.confirm("Are you sure you want to delete?") {
|
if !ConfirmService::confirm("Are you sure you want to delete?") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
log::info!("Delete achievement confirmed.");
|
log::info!("Delete achievement confirmed.");
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
|
use super::error::error_provider::ErrorContext;
|
||||||
use crate::services::rest::RestService;
|
use crate::services::rest::RestService;
|
||||||
|
use crate::services::rest::RestServiceError;
|
||||||
use common::CreateAchievement;
|
use common::CreateAchievement;
|
||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::JsCast;
|
||||||
use wasm_bindgen_futures::spawn_local;
|
use wasm_bindgen_futures::spawn_local;
|
||||||
@ -13,15 +15,16 @@ use yew_router::scope_ext::RouterScopeExt;
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Msg {
|
pub enum Msg {
|
||||||
Submit,
|
Submit,
|
||||||
Reset,
|
SubmitResult(Result<(), RestServiceError>),
|
||||||
UpdateInput(String),
|
UpdateInput(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct CreateAchievementComponent {
|
pub struct CreateAchievementComponent {
|
||||||
input_value: String,
|
input_value: String,
|
||||||
submitted: bool,
|
|
||||||
input_ref: NodeRef,
|
input_ref: NodeRef,
|
||||||
|
|
||||||
|
awaiting_response: bool,
|
||||||
}
|
}
|
||||||
impl Component for CreateAchievementComponent {
|
impl Component for CreateAchievementComponent {
|
||||||
type Message = Msg;
|
type Message = Msg;
|
||||||
@ -42,27 +45,37 @@ impl Component for CreateAchievementComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&mut self, _ctx: &yew::Context<Self>, msg: Self::Message) -> bool {
|
fn update(&mut self, ctx: &yew::Context<Self>, msg: Self::Message) -> bool {
|
||||||
match msg {
|
match msg {
|
||||||
Msg::Reset => {
|
|
||||||
self.input_value.clear();
|
|
||||||
self.submitted = false;
|
|
||||||
true
|
|
||||||
}
|
|
||||||
Msg::Submit => {
|
Msg::Submit => {
|
||||||
log::info!("Creating achievement");
|
log::info!("Creating achievement");
|
||||||
let payload = CreateAchievement {
|
let payload = CreateAchievement {
|
||||||
goal: self.input_value.clone(),
|
goal: self.input_value.clone(),
|
||||||
};
|
};
|
||||||
|
let link = ctx.link().clone();
|
||||||
spawn_local(async move {
|
spawn_local(async move {
|
||||||
match RestService::create_achievement(payload).await {
|
let res = RestService::create_achievement(payload).await;
|
||||||
Ok(_response) => {}
|
link.send_message(Msg::SubmitResult(res));
|
||||||
Err(_err) => {}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
self.awaiting_response = true;
|
||||||
|
|
||||||
self.submitted = true;
|
true
|
||||||
|
}
|
||||||
|
Msg::SubmitResult(result) => {
|
||||||
|
match result {
|
||||||
|
Ok(_response) => {
|
||||||
|
let nav = ctx.link().navigator().unwrap();
|
||||||
|
nav.push(&crate::Route::Root);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
let (error_context, _context_listener) = ctx
|
||||||
|
.link()
|
||||||
|
.context(Callback::from(|_: ErrorContext| ()))
|
||||||
|
.expect("No Error Context Provided");
|
||||||
|
error_context.dispatch(err.to_string());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.awaiting_response = false;
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
Msg::UpdateInput(value) => {
|
Msg::UpdateInput(value) => {
|
||||||
@ -76,65 +89,39 @@ impl Component for CreateAchievementComponent {
|
|||||||
let link = ctx.link().clone();
|
let link = ctx.link().clone();
|
||||||
let nav = ctx.link().navigator().unwrap();
|
let nav = ctx.link().navigator().unwrap();
|
||||||
|
|
||||||
if self.submitted {
|
let onclick_go_back = Callback::from(move |_: web_sys::MouseEvent| {
|
||||||
let onclick_go_back = Callback::from(move |_: web_sys::MouseEvent| {
|
nav.push(&crate::Route::Root);
|
||||||
nav.push(&crate::Route::Root);
|
});
|
||||||
});
|
|
||||||
|
|
||||||
let onclick_add_another = link.callback(|_: web_sys::MouseEvent| Msg::Reset);
|
let onsubmit = link.callback(|e: web_sys::SubmitEvent| {
|
||||||
|
e.prevent_default();
|
||||||
|
Msg::Submit
|
||||||
|
});
|
||||||
|
|
||||||
html! {
|
let oninput = link.callback(|e: web_sys::InputEvent| {
|
||||||
<>
|
let Some(input) = e
|
||||||
<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 class="six columns">
|
|
||||||
<button onclick={onclick_add_another} class="button">{"Add another"}</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()
|
.target()
|
||||||
.and_then(|t| t.dyn_into::<web_sys::HtmlInputElement>().ok()) else { unreachable!() };
|
.and_then(|t| t.dyn_into::<web_sys::HtmlInputElement>().ok()) else { unreachable!() };
|
||||||
Msg::UpdateInput(input.value())
|
Msg::UpdateInput(input.value())
|
||||||
});
|
});
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<>
|
<>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<button onclick={onclick_go_back} class="button">{"Back"}</button>
|
<button onclick={onclick_go_back} class="button">{"Back"}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form {onsubmit} >
|
||||||
|
<hr />
|
||||||
|
<div class="row">
|
||||||
|
<div class="twelve columns">
|
||||||
|
<label for="achievementInput">{"New achievement"}</label>
|
||||||
|
<input ref={self.input_ref.clone()} {oninput} value={self.input_value.clone()} class="u-full-width" type="text" id="achievementInput" />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<form {onsubmit} >
|
<input class="button-primary" type="submit" value="Submit" disabled={self.awaiting_response} />
|
||||||
<hr />
|
</form>
|
||||||
<div class="row">
|
</>
|
||||||
<div class="twelve columns">
|
|
||||||
<label for="achievementInput">{"New achievement"}</label>
|
|
||||||
<input ref={self.input_ref.clone()} {oninput} value={self.input_value.clone()} class="u-full-width" type="text" id="achievementInput" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<input class="button-primary" type="submit" value="Submit" />
|
|
||||||
</form>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,8 +22,10 @@ pub enum Msg {
|
|||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct CreateMilestoneComponent {
|
pub struct CreateMilestoneComponent {
|
||||||
input_value: String,
|
input_value: String,
|
||||||
submitted: bool,
|
|
||||||
input_ref: NodeRef,
|
input_ref: NodeRef,
|
||||||
|
|
||||||
|
/// Submitted and awaiting response
|
||||||
|
awaiting_response: bool,
|
||||||
}
|
}
|
||||||
impl Component for CreateMilestoneComponent {
|
impl Component for CreateMilestoneComponent {
|
||||||
type Message = Msg;
|
type Message = Msg;
|
||||||
@ -57,7 +59,7 @@ impl Component for CreateMilestoneComponent {
|
|||||||
let res = RestService::create_milestone(payload).await;
|
let res = RestService::create_milestone(payload).await;
|
||||||
link.send_message(Msg::SubmitResult(res));
|
link.send_message(Msg::SubmitResult(res));
|
||||||
});
|
});
|
||||||
self.submitted = true;
|
self.awaiting_response = true;
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@ -72,10 +74,10 @@ impl Component for CreateMilestoneComponent {
|
|||||||
.link()
|
.link()
|
||||||
.context(Callback::from(|_: ErrorContext| ()))
|
.context(Callback::from(|_: ErrorContext| ()))
|
||||||
.expect("No Error Context Provided");
|
.expect("No Error Context Provided");
|
||||||
log::info!("dispatching error");
|
|
||||||
error_context.dispatch(err.to_string());
|
error_context.dispatch(err.to_string());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
self.awaiting_response = false;
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
Msg::UpdateInput(value) => {
|
Msg::UpdateInput(value) => {
|
||||||
@ -89,60 +91,39 @@ impl Component for CreateMilestoneComponent {
|
|||||||
let link = ctx.link().clone();
|
let link = ctx.link().clone();
|
||||||
let nav = ctx.link().navigator().unwrap();
|
let nav = ctx.link().navigator().unwrap();
|
||||||
|
|
||||||
if self.submitted {
|
let onclick_go_back = Callback::from(move |_: web_sys::MouseEvent| {
|
||||||
let onclick_go_back = Callback::from(move |_: web_sys::MouseEvent| {
|
nav.push(&crate::Route::Root);
|
||||||
nav.push(&crate::Route::Root);
|
});
|
||||||
});
|
|
||||||
|
|
||||||
html! {
|
let onsubmit = link.callback(|e: web_sys::SubmitEvent| {
|
||||||
<>
|
e.prevent_default();
|
||||||
<div class="row" style="margin-top: 15%">
|
Msg::Submit
|
||||||
<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| {
|
let oninput = link.callback(|e: web_sys::InputEvent| {
|
||||||
e.prevent_default();
|
let Some(input) = e
|
||||||
Msg::Submit
|
|
||||||
});
|
|
||||||
|
|
||||||
let oninput = link.callback(|e: web_sys::InputEvent| {
|
|
||||||
let Some(input) = e
|
|
||||||
.target()
|
.target()
|
||||||
.and_then(|t| t.dyn_into::<web_sys::HtmlInputElement>().ok()) else { unreachable!() };
|
.and_then(|t| t.dyn_into::<web_sys::HtmlInputElement>().ok()) else { unreachable!() };
|
||||||
Msg::UpdateInput(input.value())
|
Msg::UpdateInput(input.value())
|
||||||
});
|
});
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<>
|
<>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<button onclick={onclick_go_back} class="button">{"Back"}</button>
|
<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>
|
||||||
|
</div>
|
||||||
<form {onsubmit} >
|
<input class="button-primary" type="submit" value="Submit" disabled={self.awaiting_response} />
|
||||||
<hr />
|
</form>
|
||||||
<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>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -54,12 +54,16 @@ impl ErrorComponent {
|
|||||||
match &self.error {
|
match &self.error {
|
||||||
Some(error_msg) => {
|
Some(error_msg) => {
|
||||||
html! {
|
html! {
|
||||||
<div class="error-message">
|
<>
|
||||||
{ error_msg }
|
<div class="row">
|
||||||
<button onclick={onclick_hide}>
|
<h5><i style="margin-right: 1rem" class="fas fa-triangle-exclamation" />{"Error"}</h5>
|
||||||
|
<p>{ error_msg }</p>
|
||||||
|
<button onclick={onclick_hide} class="button-primary">
|
||||||
{ "Hide" }
|
{ "Hide" }
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<hr />
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => html! {},
|
None => html! {},
|
||||||
|
@ -13,8 +13,6 @@ pub struct Props {
|
|||||||
|
|
||||||
#[function_component(MilestoneComponent)]
|
#[function_component(MilestoneComponent)]
|
||||||
pub fn milestone_component(props: &Props) -> Html {
|
pub fn milestone_component(props: &Props) -> Html {
|
||||||
let confirm_service = use_memo(|_| ConfirmService, ());
|
|
||||||
|
|
||||||
let unfilled = props.milestone.goal - props.completed_achievements.min(props.milestone.goal);
|
let unfilled = props.milestone.goal - props.completed_achievements.min(props.milestone.goal);
|
||||||
let filled = props.completed_achievements.min(props.milestone.goal);
|
let filled = props.completed_achievements.min(props.milestone.goal);
|
||||||
|
|
||||||
@ -32,7 +30,7 @@ pub fn milestone_component(props: &Props) -> Html {
|
|||||||
|
|
||||||
let uuid = props.milestone.uuid;
|
let uuid = props.milestone.uuid;
|
||||||
let onclick_delete = Callback::from(move |_| {
|
let onclick_delete = Callback::from(move |_| {
|
||||||
if !confirm_service.confirm("Are you sure you want to delete?") {
|
if !ConfirmService::confirm("Are you sure you want to delete?") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
log::info!("Delete achievement confirmed.");
|
log::info!("Delete achievement confirmed.");
|
||||||
@ -48,7 +46,29 @@ pub fn milestone_component(props: &Props) -> Html {
|
|||||||
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">
|
||||||
{format!("{} / {}", props.completed_achievements, props.milestone.goal)}
|
{
|
||||||
|
if unfilled == 0 {
|
||||||
|
// html! { <i style="color: var(--secondary-color)" class="fas fa-gift" /> }
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
{"🎉"}
|
||||||
|
<span style="display: inline-block; width: 0.5em"></span>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
} else { html! {} }
|
||||||
|
}
|
||||||
|
{ format!("{} / {}", filled, filled + unfilled) }
|
||||||
|
{
|
||||||
|
if unfilled == 0 {
|
||||||
|
// html! { <i style="color: var(--secondary-color)" class="fas fa-gift" /> }
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<span style="display: inline-block; width: 0.5em"></span>
|
||||||
|
{"🎉"}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
} else { html! {} }
|
||||||
|
}
|
||||||
<br />
|
<br />
|
||||||
{filled_stars}
|
{filled_stars}
|
||||||
{unfilled_stars}
|
{unfilled_stars}
|
||||||
|
@ -1,121 +1,77 @@
|
|||||||
use crate::components::achievement::AchievementComponent;
|
use crate::components::achievement::AchievementComponent;
|
||||||
use crate::components::milestone::MilestoneComponent;
|
use crate::components::milestone::MilestoneComponent;
|
||||||
use crate::event_bus::EventBus;
|
use yew::functional::*;
|
||||||
use crate::services::websocket::WebsocketService;
|
|
||||||
use std::rc::Rc;
|
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
use yew_agent::Bridge;
|
use yew_router::prelude::*;
|
||||||
use yew_agent::Bridged;
|
|
||||||
use yew_router::scope_ext::RouterScopeExt;
|
|
||||||
|
|
||||||
pub struct Root {
|
#[function_component]
|
||||||
_wss: WebsocketService,
|
pub fn Root() -> Html {
|
||||||
_producer: Box<dyn Bridge<EventBus>>,
|
let nav = use_navigator().expect("cannot get navigator");
|
||||||
|
let app_state = use_context::<crate::AppState>().expect("no app state ctx found");
|
||||||
|
|
||||||
/// None until the first websocket message is received.
|
let onclick_create_achievement = {
|
||||||
state: Option<common::State>,
|
let nav = nav.clone();
|
||||||
}
|
Callback::from(move |_: web_sys::MouseEvent| {
|
||||||
|
nav.push(&crate::Route::CreateAchievement);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
pub enum Msg {
|
let onclick_create_milestone = Callback::from(move |_: web_sys::MouseEvent| {
|
||||||
HandleMsg(String),
|
nav.push(&crate::Route::CreateMilestone);
|
||||||
}
|
});
|
||||||
|
|
||||||
impl Component for Root {
|
let achievements = app_state
|
||||||
type Message = Msg;
|
.state
|
||||||
type Properties = ();
|
.achievements
|
||||||
|
.iter()
|
||||||
fn create(ctx: &Context<Self>) -> Self {
|
.cloned()
|
||||||
let wss = WebsocketService::new();
|
.map(|a| {
|
||||||
|
html! {
|
||||||
let cb = {
|
<AchievementComponent achievement={a} />
|
||||||
let link = ctx.link().clone();
|
|
||||||
move |e| link.send_message(Msg::HandleMsg(e))
|
|
||||||
};
|
|
||||||
|
|
||||||
Self {
|
|
||||||
_wss: wss,
|
|
||||||
_producer: EventBus::bridge(Rc::new(cb)),
|
|
||||||
state: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
|
|
||||||
match msg {
|
|
||||||
Msg::HandleMsg(s) => {
|
|
||||||
let msg: common::WebSocketMessage = serde_json::from_str(&s).unwrap();
|
|
||||||
self.state = Some(msg);
|
|
||||||
true
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}
|
.collect::<Html>();
|
||||||
|
|
||||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
let completed_achievements = app_state
|
||||||
let nav = ctx.link().navigator().unwrap();
|
.state
|
||||||
|
.achievements
|
||||||
|
.iter()
|
||||||
|
.filter(|a| a.completed)
|
||||||
|
.count();
|
||||||
|
|
||||||
let onclick_create_achievement = {
|
let mut milestones = app_state.state.milestones.clone();
|
||||||
let nav = nav.clone();
|
milestones.sort_by_key(|m| m.goal);
|
||||||
Callback::from(move |_: web_sys::MouseEvent| {
|
let milestones = milestones.into_iter().map(|m| html! { <MilestoneComponent milestone={m} completed_achievements={completed_achievements} /> }).collect::<Html>();
|
||||||
nav.push(&crate::Route::CreateAchievement);
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
let onclick_create_milestone = Callback::from(move |_: web_sys::MouseEvent| {
|
html! {
|
||||||
nav.push(&crate::Route::CreateMilestone);
|
<>
|
||||||
});
|
|
||||||
|
|
||||||
let Some(state) = &self.state else {
|
<h1>{"Polter A"}<span style="color: var(--danger-color); text-decoration: line-through">{"bend"}</span>{"chievements!"}</h1>
|
||||||
return html! {
|
|
||||||
<p>{"loading..."}</p>
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
let achievements = state
|
<hr />
|
||||||
.achievements
|
<div class="row flex flex-wrap">
|
||||||
.iter()
|
<div class="flex-grow">
|
||||||
.cloned()
|
<h3>{"Milestones"}</h3>
|
||||||
.map(|a| {
|
</div>
|
||||||
html! {
|
|
||||||
<AchievementComponent achievement={a} />
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Html>();
|
|
||||||
|
|
||||||
let completed_achievements = state.achievements.iter().filter(|a| a.completed).count();
|
<div class="flex-intrinsic-size">
|
||||||
|
<button onclick={onclick_create_milestone} class="button-primary">{"New"}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{milestones}
|
||||||
|
|
||||||
let mut milestones = state.milestones.clone();
|
<hr />
|
||||||
milestones.sort_by_key(|m| m.goal);
|
<div class="row flex flex-wrap">
|
||||||
let milestones = milestones.into_iter().map(|m| html! { <MilestoneComponent milestone={m} completed_achievements={completed_achievements} /> }).collect::<Html>();
|
<div class="flex-grow">
|
||||||
|
<h3>{"Achievements"}</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
html! {
|
<div class="flex-intrinsic-size">
|
||||||
<>
|
<button onclick={onclick_create_achievement} class="button-primary">{"New"}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{achievements}
|
||||||
|
|
||||||
<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"}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{achievements}
|
|
||||||
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
use serde::de::DeserializeOwned;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
@ -5,23 +6,30 @@ use yew_agent::HandlerId;
|
|||||||
use yew_agent::Public;
|
use yew_agent::Public;
|
||||||
use yew_agent::WorkerLink;
|
use yew_agent::WorkerLink;
|
||||||
|
|
||||||
pub struct EventBus {
|
pub struct EventBus<T>
|
||||||
|
where
|
||||||
|
T: 'static + Serialize + DeserializeOwned + Clone,
|
||||||
|
{
|
||||||
link: WorkerLink<Self>,
|
link: WorkerLink<Self>,
|
||||||
subscribers: HashSet<HandlerId>,
|
subscribers: HashSet<HandlerId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub enum EventBusInput {
|
pub enum EventBusInput<T> {
|
||||||
EventBusMsg(String),
|
EventBusMsg(T),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl yew_agent::Worker for EventBus {
|
impl<T> yew_agent::Worker for EventBus<T>
|
||||||
|
where
|
||||||
|
T: 'static + Serialize + DeserializeOwned + Clone,
|
||||||
|
{
|
||||||
type Reach = Public<Self>;
|
type Reach = Public<Self>;
|
||||||
type Input = EventBusInput;
|
type Input = EventBusInput<T>;
|
||||||
type Output = String;
|
type Output = T;
|
||||||
type Message = ();
|
type Message = ();
|
||||||
|
|
||||||
fn create(link: WorkerLink<Self>) -> Self {
|
fn create(link: WorkerLink<Self>) -> Self {
|
||||||
|
log::debug!("Creating Event Bus.");
|
||||||
Self {
|
Self {
|
||||||
link,
|
link,
|
||||||
subscribers: HashSet::new(),
|
subscribers: HashSet::new(),
|
||||||
@ -32,9 +40,13 @@ impl yew_agent::Worker for EventBus {
|
|||||||
|
|
||||||
fn handle_input(&mut self, msg: Self::Input, _id: HandlerId) {
|
fn handle_input(&mut self, msg: Self::Input, _id: HandlerId) {
|
||||||
match msg {
|
match msg {
|
||||||
EventBusInput::EventBusMsg(s) => {
|
EventBusInput::EventBusMsg(msg) => {
|
||||||
|
log::debug!(
|
||||||
|
"Event bus received message. Responding to {} subscribers.",
|
||||||
|
self.subscribers.len()
|
||||||
|
);
|
||||||
for sub in &self.subscribers {
|
for sub in &self.subscribers {
|
||||||
self.link.respond(*sub, s.clone());
|
self.link.respond(*sub, msg.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
use crate::components::error::error_component::ErrorComponent;
|
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::services::websocket::WebsocketService;
|
||||||
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;
|
||||||
use components::root::Root;
|
use components::root::Root;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
|
use yew_agent::Bridged;
|
||||||
use yew_router::BrowserRouter;
|
use yew_router::BrowserRouter;
|
||||||
use yew_router::Routable;
|
use yew_router::Routable;
|
||||||
use yew_router::Switch;
|
use yew_router::Switch;
|
||||||
@ -41,16 +44,39 @@ fn switch(selected_route: Route) -> Html {
|
|||||||
|
|
||||||
#[derive(Default, Clone, Debug, PartialEq, Eq)]
|
#[derive(Default, Clone, Debug, PartialEq, Eq)]
|
||||||
struct AppStateInner {
|
struct AppStateInner {
|
||||||
|
/// The part of the state that comes from the server
|
||||||
state: common::State,
|
state: common::State,
|
||||||
}
|
}
|
||||||
|
impl Reducible for AppStateInner {
|
||||||
|
type Action = common::WebSocketMessage;
|
||||||
|
fn reduce(self: Rc<Self>, action: Self::Action) -> Rc<Self> {
|
||||||
|
Rc::new(Self { state: action })
|
||||||
|
}
|
||||||
|
}
|
||||||
type AppState = Rc<AppStateInner>;
|
type AppState = Rc<AppStateInner>;
|
||||||
|
|
||||||
#[function_component]
|
#[function_component]
|
||||||
pub fn App() -> Html {
|
pub fn App() -> Html {
|
||||||
let ctx = use_state(|| Rc::new(AppStateInner::default()));
|
let app_state = use_reducer(AppStateInner::default);
|
||||||
|
let app_state_dispatcher = app_state.dispatcher();
|
||||||
|
|
||||||
|
let _event_bus = use_memo(
|
||||||
|
|_| {
|
||||||
|
log::info!("Creating event bus bridge.");
|
||||||
|
EventBus::<common::WebSocketMessage>::bridge(Rc::new(move |ws_msg: common::WebSocketMessage| {
|
||||||
|
log::debug!("dispatching websocket msg to reducer");
|
||||||
|
app_state_dispatcher.dispatch(ws_msg);
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let _wss = use_memo(|_| Rc::new(WebsocketService::connect()), ());
|
||||||
|
|
||||||
|
let rc_app_state = Rc::new((*app_state).clone());
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<ContextProvider<AppState> context={(*ctx).clone()}>
|
<ContextProvider<AppState> context={rc_app_state}>
|
||||||
<ErrorProvider>
|
<ErrorProvider>
|
||||||
<div class="container" style="margin-top: 5%; margin-bottom: 25%">
|
<div class="container" style="margin-top: 5%; margin-bottom: 25%">
|
||||||
<ErrorComponent />
|
<ErrorComponent />
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
pub struct ConfirmService;
|
pub struct ConfirmService;
|
||||||
|
|
||||||
impl ConfirmService {
|
impl ConfirmService {
|
||||||
pub fn confirm(&self, message: &str) -> bool {
|
pub fn confirm(message: &str) -> bool {
|
||||||
web_sys::window()
|
web_sys::window()
|
||||||
.expect("no global `window` exists")
|
.expect("no global `window` exists")
|
||||||
.confirm_with_message(message)
|
.confirm_with_message(message)
|
||||||
|
@ -2,8 +2,10 @@ use common::CreateAchievement;
|
|||||||
use common::CreateMilestone;
|
use common::CreateMilestone;
|
||||||
use common::DeleteAchievement;
|
use common::DeleteAchievement;
|
||||||
use common::DeleteMilestone;
|
use common::DeleteMilestone;
|
||||||
|
use common::RestResponse;
|
||||||
use common::ToggleAchievement;
|
use common::ToggleAchievement;
|
||||||
use reqwasm::http::Request;
|
use reqwasm::http::Request;
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
pub struct RestService;
|
pub struct RestService;
|
||||||
@ -13,11 +15,14 @@ pub enum RestServiceError {
|
|||||||
#[error("Failed to serialize/deserialize data: {0}")]
|
#[error("Failed to serialize/deserialize data: {0}")]
|
||||||
SerdeError(#[from] serde_json::Error),
|
SerdeError(#[from] serde_json::Error),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
RestError(#[from] common::RestError),
|
||||||
|
|
||||||
|
#[error("Failed request: {0}")]
|
||||||
|
ReqwasmError(#[from] reqwasm::Error),
|
||||||
|
|
||||||
#[error("Unexpected response")]
|
#[error("Unexpected response")]
|
||||||
UnexpectedResponse(reqwasm::http::Response),
|
UnexpectedResponse(reqwasm::http::Response),
|
||||||
|
|
||||||
#[error("Failed request")]
|
|
||||||
ReqwasmError(#[from] reqwasm::Error),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RestService {
|
impl RestService {
|
||||||
@ -29,31 +34,26 @@ impl RestService {
|
|||||||
format!("http://{}:4000{endpoint}", Self::hostname())
|
format!("http://{}:4000{endpoint}", Self::hostname())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn post_json<T: Serialize>(payload: T, path: &str) -> Result<(), RestServiceError> {
|
async fn post_json<P: Serialize, T: DeserializeOwned>(
|
||||||
|
payload: P,
|
||||||
|
path: &str,
|
||||||
|
) -> Result<T, RestServiceError> {
|
||||||
let req = Request::post(&Self::url_to(path))
|
let req = Request::post(&Self::url_to(path))
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.body(serde_json::to_string(&payload)?);
|
.body(serde_json::to_string(&payload)?);
|
||||||
|
|
||||||
let res = req.send().await;
|
let response = req.send().await.map_err(|err| {
|
||||||
match res {
|
log::error!("Request failed: {:?}", err);
|
||||||
Ok(response) => {
|
err
|
||||||
let status_code = response.status();
|
})?;
|
||||||
if (200..300).contains(&status_code) {
|
|
||||||
Ok(())
|
if let Ok(rest_response) = response.json::<RestResponse<T>>().await {
|
||||||
} else {
|
return rest_response.map_err(Into::into);
|
||||||
log::warn!(
|
|
||||||
"Received unexpected status code: {}. Response: {:?}",
|
|
||||||
status_code,
|
|
||||||
response
|
|
||||||
);
|
|
||||||
Err(RestServiceError::UnexpectedResponse(response))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
log::error!("Request failed: {:?}", err);
|
|
||||||
Err(err.into())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let err = RestServiceError::UnexpectedResponse(response);
|
||||||
|
log::warn!("{err:?}");
|
||||||
|
Err(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn toggle_achievement(payload: ToggleAchievement) -> Result<(), RestServiceError> {
|
pub async fn toggle_achievement(payload: ToggleAchievement) -> Result<(), RestServiceError> {
|
||||||
|
@ -1,49 +1,57 @@
|
|||||||
use crate::event_bus::EventBus;
|
use crate::event_bus::EventBus;
|
||||||
use crate::event_bus::EventBusInput;
|
use crate::event_bus::EventBusInput;
|
||||||
use futures::channel::mpsc::Sender;
|
use futures::channel::mpsc::Sender;
|
||||||
use futures::SinkExt;
|
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use reqwasm::websocket::futures::WebSocket;
|
use reqwasm::websocket::futures::WebSocket;
|
||||||
use reqwasm::websocket::Message;
|
use reqwasm::websocket::Message;
|
||||||
use wasm_bindgen_futures::spawn_local;
|
use wasm_bindgen_futures::spawn_local;
|
||||||
use yew_agent::Dispatched;
|
use yew_agent::Dispatched;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct WebsocketService {
|
pub struct WebsocketService {
|
||||||
pub tx: Sender<String>,
|
// No messages sent from app to server on websocket at the moment.
|
||||||
|
pub tx: Sender<()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WebsocketService {
|
impl WebsocketService {
|
||||||
pub fn new() -> Self {
|
pub fn connect() -> Self {
|
||||||
let hostname = web_sys::window().unwrap().location().hostname().unwrap();
|
let hostname = web_sys::window().unwrap().location().hostname().unwrap();
|
||||||
let ws_addr = format!("ws://{hostname}:4000/ws");
|
let ws_addr = format!("ws://{hostname}:4000/ws");
|
||||||
let ws = WebSocket::open(&ws_addr).unwrap();
|
let ws = WebSocket::open(&ws_addr).unwrap();
|
||||||
|
log::info!("Opened websocket connection to {ws_addr}");
|
||||||
|
|
||||||
let (mut write, mut read) = ws.split();
|
let (_write, mut read) = ws.split();
|
||||||
|
|
||||||
let (in_tx, mut in_rx) = futures::channel::mpsc::channel::<String>(1000);
|
let (in_tx, mut in_rx) = futures::channel::mpsc::channel::<()>(1000);
|
||||||
let mut event_bus = EventBus::dispatcher();
|
let mut event_bus = EventBus::<common::WebSocketMessage>::dispatcher();
|
||||||
|
|
||||||
|
// App to Server
|
||||||
spawn_local(async move {
|
spawn_local(async move {
|
||||||
while let Some(s) = in_rx.next().await {
|
while let Some(()) = in_rx.next().await {
|
||||||
log::debug!("got event from channel! {}", s);
|
// write.send(Message::Text(s)).await.unwrap();
|
||||||
write.send(Message::Text(s)).await.unwrap();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Server to App
|
||||||
spawn_local(async move {
|
spawn_local(async move {
|
||||||
while let Some(msg) = read.next().await {
|
while let Some(msg) = read.next().await {
|
||||||
match msg {
|
match msg {
|
||||||
Ok(Message::Text(data)) => {
|
Ok(Message::Text(data)) => {
|
||||||
event_bus.send(EventBusInput::EventBusMsg(data));
|
match serde_json::from_str::<common::WebSocketMessage>(&data) {
|
||||||
}
|
Ok(ws_msg) => {
|
||||||
Ok(Message::Bytes(b)) => {
|
log::debug!("Received ws message. Dispatching to event bus.");
|
||||||
let decoded = std::str::from_utf8(&b);
|
event_bus.send(EventBusInput::EventBusMsg(ws_msg));
|
||||||
if let Ok(val) = decoded {
|
}
|
||||||
event_bus.send(EventBusInput::EventBusMsg(val.into()));
|
Err(err) => {
|
||||||
|
log::error!("{err:?}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Ok(Message::Bytes(_)) => {
|
||||||
log::error!("ws: {:?}", e);
|
log::warn!("Received binary data on websocket. This is unhandled");
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log::error!("ws error: {err:?}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -73,3 +73,8 @@
|
|||||||
.flex-grow {
|
.flex-grow {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input:disabled {
|
||||||
|
filter: brightness(75%);
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user