💄 Rework Login component
This commit is contained in:
@@ -1,131 +1,110 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use constcat::concat as const_concat;
|
||||
use dioxus::prelude::*;
|
||||
use fermi::*;
|
||||
use tracing::debug;
|
||||
use rand::distributions::{Alphanumeric, DistString};
|
||||
use tracing::{debug, error, warn};
|
||||
|
||||
use crate::base::SESSION;
|
||||
use crate::components::avatar_selector::AvatarSelector;
|
||||
use crate::components::header::Header;
|
||||
use crate::base::{Session, SESSION};
|
||||
|
||||
use super::button::{LoginButton, RegisterButton};
|
||||
use super::spinner::Spinner;
|
||||
use super::text_field::TextField;
|
||||
|
||||
use super::wallpaper::Wallpaper;
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/style_vars.rs"));
|
||||
|
||||
turf::style_sheet!("src/components/login.scss");
|
||||
|
||||
static EMPTY_PLACEHOLDER: &str = "Tmp placeholder";
|
||||
const SEP: &str = ",";
|
||||
|
||||
pub fn Login(cx: Scope) -> Element {
|
||||
debug!("Login rendering");
|
||||
const BACKGROUND_COLORS_STR: &str = const_concat!(
|
||||
style::COLOR_PRIMARY_150,
|
||||
SEP,
|
||||
style::COLOR_PRIMARY_140,
|
||||
SEP,
|
||||
style::COLOR_SECONDARY_150,
|
||||
SEP,
|
||||
style::COLOR_SECONDARY_140,
|
||||
SEP,
|
||||
style::COLOR_TERNARY_150,
|
||||
SEP,
|
||||
style::COLOR_TERNARY_140,
|
||||
);
|
||||
|
||||
let session = use_atom_ref(cx, &SESSION);
|
||||
const SHAPE_1_COLORS_STR: &str = const_concat!(
|
||||
style::COLOR_PRIMARY_120,
|
||||
SEP,
|
||||
style::COLOR_PRIMARY_110,
|
||||
SEP,
|
||||
style::COLOR_PRIMARY_100,
|
||||
SEP,
|
||||
style::COLOR_PRIMARY_90,
|
||||
SEP,
|
||||
style::COLOR_PRIMARY_80,
|
||||
);
|
||||
|
||||
let invalid_login = use_state(cx, || false);
|
||||
const SHAPE_2_COLORS_STR: &str = const_concat!(
|
||||
style::COLOR_SECONDARY_120,
|
||||
SEP,
|
||||
style::COLOR_SECONDARY_110,
|
||||
SEP,
|
||||
style::COLOR_SECONDARY_100,
|
||||
SEP,
|
||||
style::COLOR_SECONDARY_90,
|
||||
SEP,
|
||||
style::COLOR_SECONDARY_80,
|
||||
);
|
||||
|
||||
let login = use_ref(cx, Login::new);
|
||||
const SHAPE_3_COLORS_STR: &str = const_concat!(
|
||||
style::COLOR_TERNARY_120,
|
||||
SEP,
|
||||
style::COLOR_TERNARY_110,
|
||||
SEP,
|
||||
style::COLOR_TERNARY_100,
|
||||
SEP,
|
||||
style::COLOR_TERNARY_90,
|
||||
SEP,
|
||||
style::COLOR_TERNARY_80,
|
||||
);
|
||||
|
||||
let password_class = if **invalid_login {
|
||||
ClassName::INVALID_INPUT
|
||||
} else {
|
||||
""
|
||||
async fn generate_random_avatar(url: &String) -> Option<String> {
|
||||
let seed = Alphanumeric.sample_string(&mut rand::thread_rng(), 16);
|
||||
let req = format!(
|
||||
"https://{url}/7.x/shapes/svg?\
|
||||
seed={seed}&\
|
||||
backgroundColor={BACKGROUND_COLORS_STR}&\
|
||||
shape1Color={SHAPE_1_COLORS_STR}&\
|
||||
shape2Color={SHAPE_2_COLORS_STR}&\
|
||||
shape3Color={SHAPE_3_COLORS_STR}"
|
||||
);
|
||||
|
||||
let mut res: Option<String> = None;
|
||||
match reqwest::get(req).await {
|
||||
Ok(result) => {
|
||||
match result.text().await {
|
||||
Ok(svg) => {
|
||||
res = Some(svg);
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Error during placeholder loading: {}", err);
|
||||
}
|
||||
};
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Error during placeholder loading: {}", err);
|
||||
}
|
||||
};
|
||||
|
||||
let run_matrix_client = move |_| {
|
||||
cx.spawn({
|
||||
|
||||
to_owned![session, login];
|
||||
|
||||
async move {
|
||||
let login_ref = login.read();
|
||||
|
||||
session.write().update(
|
||||
login_ref.homeserver_url.clone(),
|
||||
login_ref.email.clone(),
|
||||
login_ref.password.clone(),
|
||||
);
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let login_ref = login.read();
|
||||
let placeholder = EMPTY_PLACEHOLDER.to_string();
|
||||
let homeserver_url_value = login_ref.homeserver_url.as_ref().unwrap_or(&placeholder);
|
||||
let email_value = login_ref.email.as_ref().unwrap_or(&placeholder);
|
||||
let password_value = login_ref.password.as_ref().unwrap_or(&placeholder);
|
||||
|
||||
cx.render(rsx! {
|
||||
style { STYLE_SHEET },
|
||||
|
||||
div {
|
||||
class: ClassName::ROOT,
|
||||
|
||||
div {
|
||||
class: ClassName::HEADER,
|
||||
Header {},
|
||||
},
|
||||
|
||||
div {
|
||||
class: ClassName::BODY,
|
||||
div {
|
||||
class: ClassName::AVATAR_SELECTOR_CONTAINER,
|
||||
AvatarSelector {},
|
||||
},
|
||||
|
||||
p {
|
||||
"Matrix homeserver:"
|
||||
},
|
||||
input {
|
||||
id: "input-homeserver-url",
|
||||
r#type: "text",
|
||||
name: "homeserver URL",
|
||||
value: "{homeserver_url_value}",
|
||||
oninput: move |evt| login.write().homeserver_url = Some(evt.value.clone()),
|
||||
},
|
||||
|
||||
p {
|
||||
"E-mail address:"
|
||||
},
|
||||
input {
|
||||
id: "login-input-email",
|
||||
r#type: "text",
|
||||
name: "email",
|
||||
value: "{email_value}",
|
||||
oninput: move |evt| login.write().email = Some(evt.value.clone()),
|
||||
},
|
||||
p {
|
||||
"Password:"
|
||||
},
|
||||
input {
|
||||
class: "{password_class}",
|
||||
id: "login-input-password",
|
||||
r#type: "password",
|
||||
name: "Password",
|
||||
value: "{password_value}",
|
||||
oninput: move |evt| {
|
||||
login.write().password = Some(evt.value.clone());
|
||||
invalid_login.set(false);
|
||||
},
|
||||
},
|
||||
|
||||
div {
|
||||
class: ClassName::FOOTER_BUTTONS,
|
||||
input {
|
||||
class: ClassName::BUTTON,
|
||||
onclick: run_matrix_client,
|
||||
r#type: "submit",
|
||||
value: "sign in",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
res
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Login {
|
||||
struct LoginData {
|
||||
homeserver_url: Option<String>,
|
||||
email: Option<String>,
|
||||
password: Option<String>,
|
||||
}
|
||||
|
||||
impl Login {
|
||||
impl LoginData {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
homeserver_url: None,
|
||||
@@ -134,3 +113,163 @@ impl Login {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn on_login(session_ref: &UseAtomRef<Session>, login_ref: &UseRef<LoginData>) {
|
||||
let login = login_ref.read();
|
||||
|
||||
session_ref.write().update(
|
||||
login.homeserver_url.clone(),
|
||||
login.email.clone(),
|
||||
login.password.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Props)]
|
||||
pub struct LoginProps<'a> {
|
||||
dicebear_hostname: Option<&'a str>,
|
||||
}
|
||||
|
||||
pub fn Login<'a>(cx: Scope<'a, LoginProps>) -> Element<'a> {
|
||||
debug!("Login rendering");
|
||||
|
||||
let session = use_atom_ref(cx, &SESSION);
|
||||
|
||||
let login = use_ref(cx, LoginData::new);
|
||||
let login_ref = login.read();
|
||||
let homeserver_url = login_ref.homeserver_url.as_deref().unwrap_or("");
|
||||
let email = login_ref.email.as_deref().unwrap_or("");
|
||||
let password = login_ref.password.as_deref().unwrap_or("");
|
||||
|
||||
// TODO: Enable the spinner for registration and login steps.
|
||||
let spinner_animated = use_state(cx, || false);
|
||||
|
||||
let url = cx
|
||||
.props
|
||||
.dicebear_hostname
|
||||
.unwrap_or("dicebear.tools.adrien.run")
|
||||
.to_string();
|
||||
|
||||
let random_avatar_future =
|
||||
use_future(
|
||||
cx,
|
||||
&url,
|
||||
|url| async move { generate_random_avatar(&url).await },
|
||||
);
|
||||
|
||||
let avatar = match random_avatar_future.value() {
|
||||
Some(Some(svg)) => {
|
||||
rsx!(div {
|
||||
class: ClassName::CONTENT,
|
||||
dangerous_inner_html: svg.as_str(),
|
||||
})
|
||||
}
|
||||
Some(None) | None => {
|
||||
warn!("No profile image set or generated, display the placeholder");
|
||||
rsx!(div {
|
||||
class: ClassName::CONTENT,
|
||||
img {
|
||||
src: "./images/login-profile-placeholder.svg"
|
||||
}
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
if **spinner_animated && session.read().is_logged {
|
||||
debug!("Stop spinner");
|
||||
spinner_animated.set(false);
|
||||
}
|
||||
|
||||
let on_login = move |_| {
|
||||
cx.spawn({
|
||||
|
||||
to_owned![login, session, spinner_animated];
|
||||
|
||||
async move {
|
||||
spinner_animated.set(true);
|
||||
on_login(&session, &login).await;
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
cx.render(rsx! {
|
||||
style { STYLE_SHEET },
|
||||
|
||||
Wallpaper {},
|
||||
|
||||
div {
|
||||
class: ClassName::ROOT,
|
||||
|
||||
div {
|
||||
class: ClassName::FORM,
|
||||
|
||||
div {
|
||||
class: ClassName::PHOTO,
|
||||
|
||||
onclick: move |_| {
|
||||
random_avatar_future.restart()
|
||||
},
|
||||
|
||||
{avatar},
|
||||
},
|
||||
|
||||
div {
|
||||
class: ClassName::HOMESERVER,
|
||||
TextField {
|
||||
id: "hs_url",
|
||||
r#type: "text",
|
||||
placeholder: "Homeserver URL",
|
||||
value: "{homeserver_url}",
|
||||
oninput: move |evt: FormEvent| login.write().homeserver_url = Some(evt.value.clone()),
|
||||
},
|
||||
},
|
||||
|
||||
div {
|
||||
class: ClassName::EMAIL,
|
||||
TextField {
|
||||
id: "email",
|
||||
r#type: "email",
|
||||
placeholder: "Email",
|
||||
value: "{email}",
|
||||
is_value_valid: false,
|
||||
oninput: move |evt: FormEvent| login.write().email = Some(evt.value.clone()),
|
||||
},
|
||||
},
|
||||
|
||||
div {
|
||||
class: ClassName::PASSWORD,
|
||||
TextField {
|
||||
id: "password",
|
||||
r#type: "password",
|
||||
placeholder: "Password",
|
||||
value: "{password}",
|
||||
oninput: move |evt: FormEvent| login.write().password = Some(evt.value.clone()),
|
||||
},
|
||||
},
|
||||
|
||||
div {
|
||||
class: ClassName::SPINNER,
|
||||
Spinner {
|
||||
animate: **spinner_animated,
|
||||
},
|
||||
},
|
||||
|
||||
div {
|
||||
class: ClassName::REGISTER,
|
||||
RegisterButton {
|
||||
id: "register",
|
||||
// TODO: Handle the registration process
|
||||
},
|
||||
},
|
||||
|
||||
div {
|
||||
class: ClassName::LOGIN,
|
||||
LoginButton {
|
||||
id: "login",
|
||||
focus: true,
|
||||
onclick: on_login,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@@ -1,53 +1,111 @@
|
||||
@import "../_base.scss";
|
||||
@import "../_base.scss"
|
||||
@import "./spinner.scss"
|
||||
|
||||
.root {
|
||||
width: 90%;
|
||||
height: 98%;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
padding: 5%;
|
||||
padding-top: 2%;
|
||||
position: relative;
|
||||
top: -100vh;
|
||||
|
||||
background: linear-gradient(rgb(138, 191, 209), rgb(236, 246, 249) 10%);
|
||||
.form {
|
||||
$height: 95%;
|
||||
height: $height;
|
||||
|
||||
.header {
|
||||
height: 5%;
|
||||
width: 100%;
|
||||
}
|
||||
max-height: $form-max-height;
|
||||
aspect-ratio: $form-aspect-ratio;
|
||||
|
||||
.body {
|
||||
height: 50%;
|
||||
width: 50%;
|
||||
max-width: 400px;
|
||||
border: $border-big;
|
||||
border-color: $color-primary-90;
|
||||
border-radius: $border-radius;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
background-color: $greyscale-0;
|
||||
|
||||
padding-bottom: 3%;
|
||||
display: grid;
|
||||
|
||||
.invalidInput {
|
||||
border-color: red;
|
||||
}
|
||||
$padding-col: 5%;
|
||||
$button-height: 8%;
|
||||
$button-overlap: 5%;
|
||||
$profile-img-width: 40%;
|
||||
$edit-padding: 7.5%;
|
||||
$photo-padding: 17.5%;
|
||||
|
||||
.avatar-selector-container {
|
||||
height: 30%;
|
||||
width: 100%;
|
||||
$padding-row: calc(5% * $form-aspect-ratio);
|
||||
$spinner-col-width: calc(0% + ($button-overlap * 2));
|
||||
$profile-img-height: calc($profile-img-width * $form-aspect-ratio);
|
||||
$profile-img-ext-width: calc((($profile-img-width - $spinner-col-width) / 2) - $button-overlap);
|
||||
$center-width: calc((50% - $padding-col - $edit-padding - $photo-padding - $button-overlap -
|
||||
$profile-img-ext-width) * 2);
|
||||
$spinner-height: calc(($spinner-col-width + $center-width) * $form-aspect-ratio / $logo-aspect-ratio);
|
||||
|
||||
padding-left: 25%;
|
||||
}
|
||||
grid-template-columns: $padding-col $edit-padding $photo-padding
|
||||
$button-overlap $profile-img-ext-width $center-width $profile-img-ext-width $button-overlap
|
||||
$photo-padding $edit-padding $padding-col;
|
||||
grid-template-rows: $padding-row $profile-img-height auto 5% 5% 5% 5% 5% 8.5% $spinner-height 8.5% $button-height $padding-row;
|
||||
grid-template-areas:
|
||||
". . . . . . . . . . ."
|
||||
". . . photo photo photo photo photo . . ."
|
||||
". . . . . . . . . . ."
|
||||
". . homeserver homeserver homeserver homeserver homeserver homeserver homeserver . ."
|
||||
". . . . . . . . . . ."
|
||||
". . username username username username username username username . ."
|
||||
". . . . . . . . . . ."
|
||||
". . status status status status status status status . ."
|
||||
". . . . . . . . . . ."
|
||||
". . . . spinner spinner spinner . . . ."
|
||||
". . . . . . . . . . ."
|
||||
". register register register register . login login login login ."
|
||||
". . . . . . . . . . ."
|
||||
;
|
||||
|
||||
.photo {
|
||||
grid-area: photo;
|
||||
|
||||
.footerButtons {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
padding-top: 5%;
|
||||
border: $border-normal;
|
||||
border-radius: $border-radius;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
overflow: hidden;
|
||||
|
||||
.content {
|
||||
height: calc(100% + (2 * $border-big-width));
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.homeserver {
|
||||
grid-area: homeserver;
|
||||
}
|
||||
|
||||
.email {
|
||||
grid-area: username;
|
||||
}
|
||||
|
||||
.password {
|
||||
grid-area: status;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
grid-area: spinner;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.register {
|
||||
grid-area: register;
|
||||
}
|
||||
|
||||
.login {
|
||||
grid-area: login;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -4,7 +4,6 @@ use tracing::debug;
|
||||
|
||||
use crate::base::SESSION;
|
||||
use crate::components::contacts_window::ContactsWindow;
|
||||
use crate::components::login::Login;
|
||||
|
||||
pub fn MainWindow(cx: Scope) -> Element {
|
||||
debug!("MainWindow rendering");
|
||||
@@ -16,8 +15,5 @@ pub fn MainWindow(cx: Scope) -> Element {
|
||||
if is_logged {
|
||||
rsx!(ContactsWindow {})
|
||||
}
|
||||
else {
|
||||
rsx!(Login {})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
Reference in New Issue
Block a user