feat: improved the signin ux

This commit is contained in:
antifallobst 2023-10-02 20:04:10 +02:00
parent d0e119eabc
commit 93cacdb8e5
Signed by: antifallobst
GPG Key ID: 2B4F402172791BAF
5 changed files with 82 additions and 17 deletions

View File

@ -1,6 +1,6 @@
mod data; mod data;
use anyhow::Result; use anyhow::{Error, Result};
use gloo_net::http::Request; use gloo_net::http::Request;
use serde_json::json; use serde_json::json;
use web_sys::RequestMode; use web_sys::RequestMode;
@ -29,7 +29,13 @@ impl Session {
} }
} }
pub fn login(&self, username: String, password: String, callback: Callback<Result<String>>) { pub fn login(
&self,
username: String,
password: String,
callback: Callback<(Result<String>, Callback<Result<()>>)>,
status_callback: Callback<Result<()>>,
) {
let url = format!("{}/account/authenticate", &self.base_url); let url = format!("{}/account/authenticate", &self.base_url);
let body = json!({ let body = json!({
@ -39,16 +45,25 @@ impl Session {
let call = async move { let call = async move {
let request = Request::post(&url).mode(RequestMode::Cors).json(&body)?; let request = Request::post(&url).mode(RequestMode::Cors).json(&body)?;
let response = request let response = request.send().await?;
.send()
.await?
.json::<data::AccountAuthenticateResponse>()
.await?;
Ok(response.token) match response.status() {
200 => Ok(response
.json::<data::AccountAuthenticateResponse>()
.await?
.token),
400 => Err(Error::msg(format!("Bad request"))),
401 => Err(Error::msg(format!("Wrong password!"))),
403 => Err(Error::msg(format!("You're not allowed to do this!"))),
404 => Err(Error::msg(format!("Unknown username!"))),
424 => Err(Error::msg(format!("This account is not verified!"))),
e => Err(Error::msg(format!("Unknown response: {e}"))),
}
}; };
wasm_bindgen_futures::spawn_local(async move { callback.emit(call.await) }); wasm_bindgen_futures::spawn_local(
async move { callback.emit((call.await, status_callback)) },
);
} }
pub fn set_token(&mut self, token: String) { pub fn set_token(&mut self, token: String) {

View File

@ -37,7 +37,7 @@ fn switch(routes: Route) -> Html {
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq)]
pub struct Callbacks { pub struct Callbacks {
pub sign_in_callback: Callback<Result<String>>, pub sign_in_callback: Callback<(Result<String>, Callback<Result<()>>)>,
} }
#[function_component] #[function_component]
@ -47,12 +47,21 @@ fn App() -> Html {
let cloned_state = state.clone(); let cloned_state = state.clone();
let callbacks = use_state(move || Callbacks { let callbacks = use_state(move || Callbacks {
sign_in_callback: Callback::from(move |response: Result<String>| { sign_in_callback: Callback::from(
move |response: (Result<String>, Callback<Result<()>>)| {
let mut new_state = cloned_state.deref().clone(); let mut new_state = cloned_state.deref().clone();
new_state.set_token(response.expect("!!! remove this temporary expect !!!")); response.1.emit(match response.0 {
Ok(token) => {
new_state.set_token(token);
Ok(())
}
Err(e) => Err(e),
});
cloned_state.set(new_state); cloned_state.set(new_state);
}), },
),
}); });
html! { html! {

View File

@ -2,6 +2,7 @@ mod signin;
mod signup; mod signup;
use crate::topbar::Tab; use crate::topbar::Tab;
use anyhow::Result;
use signin::SignIn; use signin::SignIn;
use signup::SignUp; use signup::SignUp;
use std::rc::Rc; use std::rc::Rc;
@ -10,11 +11,16 @@ use yew::prelude::*;
pub enum Action { pub enum Action {
ToggleSignIn, ToggleSignIn,
ToggleSignUp, ToggleSignUp,
SignInStatus(String),
} }
pub struct State { pub struct State {
sign_in: bool, sign_in: bool,
sign_up: bool, sign_up: bool,
sign_in_status: AttrValue,
sign_up_status: AttrValue,
} }
impl Default for State { impl Default for State {
@ -22,6 +28,9 @@ impl Default for State {
Self { Self {
sign_in: false, sign_in: false,
sign_up: false, sign_up: false,
sign_in_status: AttrValue::default(),
sign_up_status: AttrValue::default(),
} }
} }
} }
@ -41,6 +50,11 @@ impl Reducible for State {
state.sign_in = false; state.sign_in = false;
state.sign_up = !self.sign_up; state.sign_up = !self.sign_up;
} }
Action::SignInStatus(s) => {
state.sign_in = true;
state.sign_up = false;
state.sign_in_status = s.into();
}
} }
Rc::new(state) Rc::new(state)
} }
@ -60,6 +74,14 @@ pub fn TopBar() -> Html {
Callback::from(move |_| state.dispatch(Action::ToggleSignUp)) Callback::from(move |_| state.dispatch(Action::ToggleSignUp))
}; };
let sign_in_status = {
let state = state.clone();
Callback::from(move |status: Result<()>| match status {
Ok(_) => state.dispatch(Action::ToggleSignIn),
Err(e) => state.dispatch(Action::SignInStatus(e.to_string())),
})
};
html! { html! {
<nav id="topbar"> <nav id="topbar">
<ul id="tabs-main" class="tabs"> <ul id="tabs-main" class="tabs">
@ -75,7 +97,7 @@ pub fn TopBar() -> Html {
</ul> </ul>
if state.sign_in { if state.sign_in {
<SignIn/> <SignIn status={state.sign_in_status.clone()} status_callback={sign_in_status}/>
} }
if state.sign_up { if state.sign_up {

View File

@ -1,5 +1,6 @@
use crate::backend::Session; use crate::backend::Session;
use crate::Callbacks; use crate::Callbacks;
use anyhow::Result;
use std::ops::Deref; use std::ops::Deref;
use wasm_bindgen::JsCast; use wasm_bindgen::JsCast;
use web_sys::HtmlInputElement; use web_sys::HtmlInputElement;
@ -9,10 +10,17 @@ use yew::prelude::*;
struct Data { struct Data {
pub username: String, pub username: String,
pub password: String, pub password: String,
pub status: String,
}
#[derive(Properties, PartialEq)]
pub struct Props {
pub status: AttrValue,
pub status_callback: Callback<Result<()>>,
} }
#[function_component] #[function_component]
pub fn SignIn() -> Html { pub fn SignIn(props: &Props) -> Html {
let state = use_state(|| Data::default()); let state = use_state(|| Data::default());
let callbacks = use_context::<Callbacks>().expect("Callbacks context not available!"); let callbacks = use_context::<Callbacks>().expect("Callbacks context not available!");
let session = use_context::<Session>().unwrap_or_default(); let session = use_context::<Session>().unwrap_or_default();
@ -43,6 +51,7 @@ pub fn SignIn() -> Html {
cloned_state.set(data); cloned_state.set(data);
}); });
let status_callback = props.status_callback.clone();
let onsubmit = Callback::from(move |event: SubmitEvent| { let onsubmit = Callback::from(move |event: SubmitEvent| {
event.prevent_default(); event.prevent_default();
@ -52,6 +61,7 @@ pub fn SignIn() -> Html {
state.username, state.username,
state.password, state.password,
callbacks.sign_in_callback.clone(), callbacks.sign_in_callback.clone(),
status_callback.clone(),
); );
}); });
@ -70,6 +80,10 @@ pub fn SignIn() -> Html {
<div class="center-x"> <div class="center-x">
<button class="auth-submit" type="submit">{"Sign In"}</button> <button class="auth-submit" type="submit">{"Sign In"}</button>
</div> </div>
<div class="center-x">
<p class="auth-status">{props.status.clone()}</p>
</div>
</form> </form>
} }
} }

View File

@ -107,6 +107,11 @@
outline: none; outline: none;
} }
.auth-status {
font: normal 14px Roboto;
color: #ff0000;
}
.center-x { .center-x {
text-align: center; text-align: center;
} }