💄 Rework Login component

This commit is contained in:
2024-03-10 12:02:18 +01:00
parent 1073a592ed
commit c746fb6552
5 changed files with 344 additions and 147 deletions

View File

@@ -27,6 +27,9 @@ log = "0.4.20"
tracing = "0.1.40" tracing = "0.1.40"
futures-util = "0.3.29" futures-util = "0.3.29"
futures = "0.3.29" futures = "0.3.29"
rand = "0.8.5"
reqwest = "0.11.24"
constcat = "0.5.0"
[build] [build]
target = "x86_64-unknown-linux-gnu" target = "x86_64-unknown-linux-gnu"

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" fill="none" shape-rendering="auto"><desc>"Shapes" by "Florian Körner", licensed under "CC0 1.0". / Remix of the original. - Created with dicebear.com</desc><metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><rdf:rdf><cc:work><dc:title>Shapes</dc:title><dc:creator><cc:agent rdf:about="https://www.dicebear.com"><dc:title>Florian Körner</dc:title></cc:agent></dc:creator><dc:source>https://www.dicebear.com</dc:source><cc:license rdf:resource="https://creativecommons.org/publicdomain/zero/1.0/"></cc:license></cc:work></rdf:rdf></metadata><mask id="w6sj6i8m"><rect width="100" height="100" rx="0" ry="0" x="0" y="0" fill="#fff"></rect></mask><g mask="url(#w6sj6i8m)"><rect fill="#E2F2F7" width="100" height="100" x="0" y="0"></rect><g transform="matrix(1.2 0 0 1.2 -10 -10)"><g transform="translate(51, -23) rotate(-38 50 50)"><path d="M0 0h100v100H0V0Z" fill="#83CADE"></path></g></g><g transform="matrix(.8 0 0 .8 10 10)"><g transform="translate(-2, 35) rotate(99 50 50)"><path d="M100 50A50 50 0 1 1 0 50a50 50 0 0 1 100 0Z" fill="#6957A0"></path></g></g><g transform="matrix(.4 0 0 .4 30 30)"><g transform="translate(-18, -6) rotate(-97 50 50)"><path d="m50 7 50 86.6H0L50 7Z" fill="#D53583"></path></g></g></g></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,131 +1,110 @@
use std::str::FromStr; use constcat::concat as const_concat;
use dioxus::prelude::*; use dioxus::prelude::*;
use fermi::*; use fermi::*;
use tracing::debug; use rand::distributions::{Alphanumeric, DistString};
use tracing::{debug, error, warn};
use crate::base::SESSION; use crate::base::{Session, SESSION};
use crate::components::avatar_selector::AvatarSelector;
use crate::components::header::Header; 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"); turf::style_sheet!("src/components/login.scss");
static EMPTY_PLACEHOLDER: &str = "Tmp placeholder"; const SEP: &str = ",";
pub fn Login(cx: Scope) -> Element { const BACKGROUND_COLORS_STR: &str = const_concat!(
debug!("Login rendering"); 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 { async fn generate_random_avatar(url: &String) -> Option<String> {
ClassName::INVALID_INPUT let seed = Alphanumeric.sample_string(&mut rand::thread_rng(), 16);
} else { let req = format!(
"" "https://{url}/7.x/shapes/svg?\
}; seed={seed}&\
backgroundColor={BACKGROUND_COLORS_STR}&\
let run_matrix_client = move |_| { shape1Color={SHAPE_1_COLORS_STR}&\
cx.spawn({ shape2Color={SHAPE_2_COLORS_STR}&\
shape3Color={SHAPE_3_COLORS_STR}"
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 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);
} }
})
}; };
}
let login_ref = login.read(); Err(err) => {
let placeholder = EMPTY_PLACEHOLDER.to_string(); error!("Error during placeholder loading: {}", err);
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); res
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",
},
},
},
},
})
} }
#[derive(Debug)] struct LoginData {
struct Login {
homeserver_url: Option<String>, homeserver_url: Option<String>,
email: Option<String>, email: Option<String>,
password: Option<String>, password: Option<String>,
} }
impl Login { impl LoginData {
fn new() -> Self { fn new() -> Self {
Self { Self {
homeserver_url: None, 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,
},
},
},
},
})
}

View File

@@ -1,53 +1,111 @@
@import "../_base.scss"; @import "../_base.scss"
@import "./spinner.scss"
.root { .root {
width: 90%; height: 100%;
height: 98%; width: 100%;
display: flex; display: flex;
flex-direction: column;
align-items: center; align-items: center;
padding: 5%;
padding-top: 2%;
background: linear-gradient(rgb(138, 191, 209), rgb(236, 246, 249) 10%);
.header {
height: 5%;
width: 100%;
}
.body {
height: 50%;
width: 50%;
max-width: 400px;
display: flex;
flex-direction: column;
justify-content: center; justify-content: center;
padding-bottom: 3%; position: relative;
top: -100vh;
.invalidInput { .form {
border-color: red; $height: 95%;
} height: $height;
.avatar-selector-container { max-height: $form-max-height;
height: 30%; aspect-ratio: $form-aspect-ratio;
width: 100%;
padding-left: 25%; border: $border-big;
} border-color: $color-primary-90;
border-radius: $border-radius;
background-color: $greyscale-0;
.footerButtons { display: grid;
width: 100%;
padding-top: 5%; $padding-col: 5%;
$button-height: 8%;
$button-overlap: 5%;
$profile-img-width: 40%;
$edit-padding: 7.5%;
$photo-padding: 17.5%;
$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);
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;
display: flex; display: flex;
align-items: center;
justify-content: center; justify-content: center;
border: $border-normal;
border-radius: $border-radius;
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;
} }
} }
} }

View File

@@ -4,7 +4,6 @@ use tracing::debug;
use crate::base::SESSION; use crate::base::SESSION;
use crate::components::contacts_window::ContactsWindow; use crate::components::contacts_window::ContactsWindow;
use crate::components::login::Login;
pub fn MainWindow(cx: Scope) -> Element { pub fn MainWindow(cx: Scope) -> Element {
debug!("MainWindow rendering"); debug!("MainWindow rendering");
@@ -16,8 +15,5 @@ pub fn MainWindow(cx: Scope) -> Element {
if is_logged { if is_logged {
rsx!(ContactsWindow {}) rsx!(ContactsWindow {})
} }
else {
rsx!(Login {})
}
}) })
} }