Compare commits
3 Commits
c746fb6552
...
ceeda1a771
Author | SHA1 | Date | |
---|---|---|---|
ceeda1a771
|
|||
fc0d3b1212
|
|||
01f589e789
|
@@ -29,7 +29,9 @@ futures-util = "0.3.29"
|
||||
futures = "0.3.29"
|
||||
rand = "0.8.5"
|
||||
reqwest = "0.11.24"
|
||||
constcat = "0.5.0"
|
||||
validator = { version = "0.17.0", features = ["derive"] }
|
||||
const_format = "0.2.32"
|
||||
zxcvbn = "2.2.2"
|
||||
|
||||
[build]
|
||||
target = "x86_64-unknown-linux-gnu"
|
||||
|
@@ -9,7 +9,7 @@ The goal of this project is to propose a new open-source implementation of the f
|
||||
## Back-end
|
||||
|
||||
This project is based on the [Matrix.org](https://matrix.org/) building blocks (back-end and front-end SDK) to avoid to
|
||||
reinvent the wheel. This solution provides:
|
||||
reinvent the wheel. This solution provides:
|
||||
|
||||
- [Open-source protocol](https://spec.matrix.org/v1.9/).
|
||||
- Features expected for a messaging solution in 2024 (multi-devices management, emojis, integrations, redaction,
|
||||
|
@@ -1,72 +1,50 @@
|
||||
use constcat::concat as const_concat;
|
||||
use std::borrow::Cow;
|
||||
use std::collections::hash_map::Values;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use const_format::formatcp;
|
||||
use dioxus::prelude::*;
|
||||
use fermi::*;
|
||||
use rand::distributions::{Alphanumeric, DistString};
|
||||
use tracing::{debug, error, warn};
|
||||
use validator::{Validate, ValidateArgs, ValidateEmail, ValidationError, ValidationErrors};
|
||||
use zxcvbn::zxcvbn;
|
||||
|
||||
use crate::base::{Session, SESSION};
|
||||
|
||||
use super::button::{LoginButton, RegisterButton};
|
||||
use super::spinner::Spinner;
|
||||
use super::text_field::TextField;
|
||||
use super::text_input::{TextInput, TextInputState};
|
||||
|
||||
use super::wallpaper::Wallpaper;
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/style_vars.rs"));
|
||||
|
||||
use style::{
|
||||
COLOR_PRIMARY_100, COLOR_PRIMARY_110, COLOR_PRIMARY_120, COLOR_PRIMARY_140, COLOR_PRIMARY_150,
|
||||
COLOR_PRIMARY_80, COLOR_PRIMARY_90, COLOR_SECONDARY_100, COLOR_SECONDARY_110,
|
||||
COLOR_SECONDARY_120, COLOR_SECONDARY_140, COLOR_SECONDARY_150, COLOR_SECONDARY_80,
|
||||
COLOR_SECONDARY_90, COLOR_TERNARY_100, COLOR_TERNARY_110, COLOR_TERNARY_120, COLOR_TERNARY_140,
|
||||
COLOR_TERNARY_150, COLOR_TERNARY_80, COLOR_TERNARY_90,
|
||||
};
|
||||
|
||||
turf::style_sheet!("src/components/login.scss");
|
||||
|
||||
const SEP: &str = ",";
|
||||
|
||||
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,
|
||||
const BACKGROUND_COLORS_STR: &'static str = formatcp!(
|
||||
"{COLOR_PRIMARY_150},{COLOR_PRIMARY_140},\
|
||||
{COLOR_SECONDARY_150},{COLOR_SECONDARY_140},\
|
||||
{COLOR_TERNARY_150},{COLOR_TERNARY_140}"
|
||||
);
|
||||
|
||||
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,
|
||||
const SHAPE_1_COLORS_STR: &'static str = formatcp!(
|
||||
"{COLOR_PRIMARY_120},{COLOR_PRIMARY_110},{COLOR_PRIMARY_100},{COLOR_PRIMARY_90},{COLOR_PRIMARY_80}"
|
||||
);
|
||||
|
||||
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,
|
||||
);
|
||||
const SHAPE_2_COLORS_STR: &'static str = formatcp!(
|
||||
"{COLOR_SECONDARY_120},{COLOR_SECONDARY_110},{COLOR_SECONDARY_100},{COLOR_SECONDARY_90},{COLOR_SECONDARY_80}");
|
||||
|
||||
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,
|
||||
);
|
||||
const SHAPE_3_COLORS_STR: &'static str = formatcp!(
|
||||
"{COLOR_TERNARY_120},{COLOR_TERNARY_110},{COLOR_TERNARY_100},{COLOR_TERNARY_90},{COLOR_TERNARY_80}");
|
||||
|
||||
async fn generate_random_avatar(url: &String) -> Option<String> {
|
||||
let seed = Alphanumeric.sample_string(&mut rand::thread_rng(), 16);
|
||||
@@ -98,30 +76,253 @@ async fn generate_random_avatar(url: &String) -> Option<String> {
|
||||
res
|
||||
}
|
||||
|
||||
struct LoginData {
|
||||
homeserver_url: Option<String>,
|
||||
email: Option<String>,
|
||||
password: Option<String>,
|
||||
const REQUIRED_ERROR_NAME: &'static str = "required";
|
||||
const REQUIRED_ERROR_HELPER_TEXT: &'static str = "This field must be completed";
|
||||
|
||||
const INVALID_URL_ERROR_NAME: &'static str = "url";
|
||||
const INVALID_URL_ERROR_HELPER_TEXT: &'static str = "This field must be a valid URL";
|
||||
|
||||
const INVALID_EMAIL_ERROR_NAME: &'static str = "email";
|
||||
const INVALID_EMAIL_ERROR_HELPER_TEXT: &'static str = "This field must be a valid email";
|
||||
|
||||
const TOO_WEAK_PASSWORD_ERROR_NAME: &'static str = "too_weak_password";
|
||||
const TOO_WEAK_PASSWORD_ERROR_HELPER_TEXT: &'static str = "The password is too weak";
|
||||
|
||||
const FIELDS_MISMATCH_ERROR_NAME: &'static str = "mismatch";
|
||||
|
||||
const HOMESERVER_FIELD_NAME: &'static str = "homeserver_url";
|
||||
const ID_FIELD_NAME: &'static str = "id";
|
||||
const PASSWORD_FIELD_NAME: &'static str = "password";
|
||||
const CONFIRM_PASSWORD_FIELD_NAME: &'static str = "confirm password";
|
||||
|
||||
const LOGIN_ID_PLACEHOLDER: &'static str = "Username";
|
||||
const REGISTER_ID_PLACEHOLDER: &'static str = "Email";
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum Process {
|
||||
LOGIN,
|
||||
REGISTRATION,
|
||||
}
|
||||
|
||||
impl LoginData {
|
||||
fn validate_data(data: &Data, process: &Process) -> Result<(), ValidationError> {
|
||||
match process {
|
||||
Process::REGISTRATION => {
|
||||
let mut is_confirm_password_empty = true;
|
||||
if let Some(confirm_password) = &data.confirm_password {
|
||||
if confirm_password.len() > 0 {
|
||||
is_confirm_password_empty = false;
|
||||
}
|
||||
}
|
||||
if is_confirm_password_empty {
|
||||
let mut err = ValidationError::new(&REQUIRED_ERROR_NAME);
|
||||
err.add_param(Cow::from("field_name"), &CONFIRM_PASSWORD_FIELD_NAME);
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
if data.password != data.confirm_password {
|
||||
let mut err = ValidationError::new(FIELDS_MISMATCH_ERROR_NAME);
|
||||
err.add_param(Cow::from("field_name_1"), &PASSWORD_FIELD_NAME);
|
||||
err.add_param(Cow::from("field_name_2"), &CONFIRM_PASSWORD_FIELD_NAME);
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
Process::LOGIN => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_id(id: &Option<String>, process: &Process) -> Result<(), ValidationError> {
|
||||
if *process == Process::REGISTRATION {
|
||||
if !id.validate_email() {
|
||||
let err = ValidationError::new(&INVALID_EMAIL_ERROR_NAME);
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_password(password: &Option<String>, process: &Process) -> Result<(), ValidationError> {
|
||||
if *process == Process::REGISTRATION {
|
||||
if let Some(password) = password {
|
||||
let estimate = zxcvbn(password, &[]).unwrap();
|
||||
|
||||
let score = estimate.score();
|
||||
|
||||
// TODO: Give the limit using the configuration
|
||||
if score <= 2 {
|
||||
let mut err = ValidationError::new(TOO_WEAK_PASSWORD_ERROR_NAME);
|
||||
err.add_param(Cow::from("score"), &score);
|
||||
|
||||
if let Some(feedback) = estimate.feedback() {
|
||||
let suggestions = feedback
|
||||
.suggestions()
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
err.add_param(Cow::from("suggestions"), &suggestions);
|
||||
}
|
||||
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Validate)]
|
||||
#[validate(context = "Process")]
|
||||
#[validate(schema(
|
||||
function = validate_data,
|
||||
use_context,
|
||||
skip_on_field_errors = false
|
||||
))]
|
||||
struct Data {
|
||||
#[validate(required, url)]
|
||||
homeserver_url: Option<String>,
|
||||
#[validate(required, custom(function = validate_id, use_context))]
|
||||
id: Option<String>,
|
||||
#[validate(required, custom(function = validate_password, use_context))]
|
||||
password: Option<String>,
|
||||
confirm_password: Option<String>,
|
||||
}
|
||||
|
||||
impl Data {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
homeserver_url: None,
|
||||
email: None,
|
||||
id: None,
|
||||
password: None,
|
||||
confirm_password: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn on_login(session_ref: &UseAtomRef<Session>, login_ref: &UseRef<LoginData>) {
|
||||
let login = login_ref.read();
|
||||
fn on_login(
|
||||
session_ref: &UseAtomRef<Session>,
|
||||
data_ref: &UseRef<Data>,
|
||||
) -> Result<(), ValidationErrors> {
|
||||
let login = data_ref.read();
|
||||
|
||||
session_ref.write().update(
|
||||
login.homeserver_url.clone(),
|
||||
login.email.clone(),
|
||||
login.password.clone(),
|
||||
);
|
||||
match login.validate_with_args(&Process::LOGIN) {
|
||||
Ok(_) => {
|
||||
session_ref.write().update(
|
||||
login.homeserver_url.clone(),
|
||||
login.id.clone(),
|
||||
login.password.clone(),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_register(
|
||||
_session_ref: &UseAtomRef<Session>,
|
||||
data_ref: &UseRef<Data>,
|
||||
) -> Result<(), ValidationErrors> {
|
||||
let login = data_ref.read();
|
||||
|
||||
match login.validate_with_args(&Process::REGISTRATION) {
|
||||
Ok(_) => {
|
||||
error!("TODO: Manage registration process");
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_validation_errors(
|
||||
field_errors: &HashMap<&str, &Vec<ValidationError>>,
|
||||
by_field_name_states: &HashMap<&'static str, &UseRef<TextInputState>>,
|
||||
) {
|
||||
for (field_name, errors) in field_errors {
|
||||
if let Some(state) = by_field_name_states.get(field_name) {
|
||||
for error in *errors {
|
||||
let code = error.code.to_string();
|
||||
match code.as_str() {
|
||||
REQUIRED_ERROR_NAME => {
|
||||
state
|
||||
.write()
|
||||
.invalidate(REQUIRED_ERROR_HELPER_TEXT.to_string());
|
||||
}
|
||||
INVALID_URL_ERROR_NAME => {
|
||||
state
|
||||
.write()
|
||||
.invalidate(INVALID_URL_ERROR_HELPER_TEXT.to_string());
|
||||
}
|
||||
INVALID_EMAIL_ERROR_NAME => {
|
||||
state
|
||||
.write()
|
||||
.invalidate(INVALID_EMAIL_ERROR_HELPER_TEXT.to_string());
|
||||
}
|
||||
TOO_WEAK_PASSWORD_ERROR_NAME => {
|
||||
state
|
||||
.write()
|
||||
.invalidate(TOO_WEAK_PASSWORD_ERROR_HELPER_TEXT.to_string());
|
||||
}
|
||||
other => todo!("{:?}", other),
|
||||
}
|
||||
}
|
||||
} else if *field_name == "__all__" {
|
||||
for error in *errors {
|
||||
let code = error.code.to_string();
|
||||
match code.as_str() {
|
||||
FIELDS_MISMATCH_ERROR_NAME => {
|
||||
let values = error.params.values();
|
||||
let field_names = values
|
||||
.into_iter()
|
||||
.filter_map(|v| v.as_str())
|
||||
.collect::<Vec<_>>();
|
||||
for field_name in field_names.iter() {
|
||||
if let Some(state) = by_field_name_states.get(*field_name) {
|
||||
let other_field_names = field_names
|
||||
.iter()
|
||||
.filter(|f| **f != *field_name)
|
||||
.map(|f| *f)
|
||||
.collect::<Vec<_>>();
|
||||
let other_field_names_len = other_field_names.len();
|
||||
let formatted = format!(
|
||||
"This field must match with {}{}{} field{}",
|
||||
other_field_names[0..other_field_names_len - 1].join(", "),
|
||||
if other_field_names_len > 1 {
|
||||
" and "
|
||||
} else {
|
||||
""
|
||||
},
|
||||
other_field_names[other_field_names_len - 1],
|
||||
if other_field_names_len > 1 { "s" } else { "" },
|
||||
);
|
||||
|
||||
if state.read().is_valid {
|
||||
state.write().invalidate(formatted);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
REQUIRED_ERROR_NAME => {
|
||||
if let Some(field_value) = error.params.get("field_name") {
|
||||
if let Some(field_name) = field_value.as_str() {
|
||||
if let Some(state) = by_field_name_states.get(field_name) {
|
||||
state
|
||||
.write()
|
||||
.invalidate(REQUIRED_ERROR_HELPER_TEXT.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
other => todo!("{:?}", other),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error!("No validation state found for \"{field_name}\" field name");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn reset_states(states: Values<&'static str, &UseRef<TextInputState>>) {
|
||||
let _ = states.for_each(|s| s.write().reset());
|
||||
}
|
||||
|
||||
#[derive(Props)]
|
||||
@@ -134,14 +335,29 @@ pub fn Login<'a>(cx: Scope<'a, LoginProps>) -> Element<'a> {
|
||||
|
||||
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("");
|
||||
let data_ref = use_ref(cx, Data::new);
|
||||
|
||||
let current_process = use_state(cx, || Process::LOGIN);
|
||||
|
||||
let data = data_ref.read();
|
||||
let homeserver_url = data.homeserver_url.as_deref().unwrap_or("");
|
||||
let homeserver_url_state = use_ref(cx, TextInputState::new);
|
||||
let id = data.id.as_deref().unwrap_or("");
|
||||
let id_state = use_ref(cx, TextInputState::new);
|
||||
let password = data.password.as_deref().unwrap_or("");
|
||||
let password_state = use_ref(cx, TextInputState::new);
|
||||
let confirm_password = data.confirm_password.as_deref().unwrap_or("");
|
||||
let confirm_password_state = use_ref(cx, TextInputState::new);
|
||||
|
||||
let by_field_name_states: HashMap<&'static str, &UseRef<TextInputState>> = HashMap::from([
|
||||
(HOMESERVER_FIELD_NAME, homeserver_url_state),
|
||||
(ID_FIELD_NAME, id_state),
|
||||
(PASSWORD_FIELD_NAME, password_state),
|
||||
(CONFIRM_PASSWORD_FIELD_NAME, confirm_password_state),
|
||||
]);
|
||||
|
||||
// TODO: Enable the spinner for registration and login steps.
|
||||
let spinner_animated = use_state(cx, || false);
|
||||
let id_placeholder = use_state(cx, || LOGIN_ID_PLACEHOLDER);
|
||||
|
||||
let url = cx
|
||||
.props
|
||||
@@ -179,18 +395,73 @@ pub fn Login<'a>(cx: Scope<'a, LoginProps>) -> Element<'a> {
|
||||
spinner_animated.set(false);
|
||||
}
|
||||
|
||||
let on_login = move |_| {
|
||||
cx.spawn({
|
||||
let on_login = {
|
||||
to_owned![by_field_name_states];
|
||||
|
||||
to_owned![login, session, spinner_animated];
|
||||
move |_| {
|
||||
reset_states(by_field_name_states.values());
|
||||
|
||||
async move {
|
||||
spinner_animated.set(true);
|
||||
on_login(&session, &login).await;
|
||||
if **current_process == Process::REGISTRATION {
|
||||
current_process.set(Process::LOGIN);
|
||||
data_ref.write().id = None;
|
||||
return;
|
||||
}
|
||||
})
|
||||
|
||||
spinner_animated.set(true);
|
||||
|
||||
if let Err(errors) = on_login(&session, &data_ref) {
|
||||
let field_errors = errors.field_errors();
|
||||
on_validation_errors(&field_errors, &by_field_name_states);
|
||||
}
|
||||
|
||||
spinner_animated.set(false);
|
||||
}
|
||||
};
|
||||
|
||||
let on_register = {
|
||||
to_owned![by_field_name_states];
|
||||
|
||||
move |_| {
|
||||
reset_states(by_field_name_states.values());
|
||||
|
||||
if **current_process == Process::LOGIN {
|
||||
current_process.set(Process::REGISTRATION);
|
||||
data_ref.write().id = None;
|
||||
return;
|
||||
}
|
||||
|
||||
spinner_animated.set(true);
|
||||
|
||||
if let Err(errors) = on_register(&session, &data_ref) {
|
||||
let field_errors = errors.field_errors();
|
||||
on_validation_errors(&field_errors, &by_field_name_states);
|
||||
}
|
||||
|
||||
spinner_animated.set(false);
|
||||
}
|
||||
};
|
||||
|
||||
let mut form_classes: [&str; 2] = [ClassName::FORM, ""];
|
||||
let mut confirm_password_classes: [&str; 2] = [ClassName::CONFIRM_PASSWORD, ""];
|
||||
match **current_process {
|
||||
Process::REGISTRATION => {
|
||||
form_classes[1] = ClassName::REGISTER;
|
||||
confirm_password_classes[1] = ClassName::SHOW;
|
||||
|
||||
if **id_placeholder != REGISTER_ID_PLACEHOLDER {
|
||||
id_placeholder.set(REGISTER_ID_PLACEHOLDER);
|
||||
}
|
||||
}
|
||||
Process::LOGIN => {
|
||||
if **id_placeholder != LOGIN_ID_PLACEHOLDER {
|
||||
id_placeholder.set(LOGIN_ID_PLACEHOLDER);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let form_classes_str = form_classes.join(" ");
|
||||
let confirm_password_classes_str = confirm_password_classes.join(" ");
|
||||
|
||||
cx.render(rsx! {
|
||||
style { STYLE_SHEET },
|
||||
|
||||
@@ -200,7 +471,7 @@ pub fn Login<'a>(cx: Scope<'a, LoginProps>) -> Element<'a> {
|
||||
class: ClassName::ROOT,
|
||||
|
||||
div {
|
||||
class: ClassName::FORM,
|
||||
class: "{form_classes_str}",
|
||||
|
||||
div {
|
||||
class: ClassName::PHOTO,
|
||||
@@ -214,35 +485,50 @@ pub fn Login<'a>(cx: Scope<'a, LoginProps>) -> Element<'a> {
|
||||
|
||||
div {
|
||||
class: ClassName::HOMESERVER,
|
||||
TextField {
|
||||
TextInput {
|
||||
id: "hs_url",
|
||||
r#type: "text",
|
||||
placeholder: "Homeserver URL",
|
||||
value: "{homeserver_url}",
|
||||
oninput: move |evt: FormEvent| login.write().homeserver_url = Some(evt.value.clone()),
|
||||
state: homeserver_url_state,
|
||||
oninput: move |evt: FormEvent| data_ref.write().homeserver_url = if evt.value.len() > 0 {
|
||||
Some(evt.value.clone()) } else { None },
|
||||
},
|
||||
},
|
||||
|
||||
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()),
|
||||
class: ClassName::ID,
|
||||
TextInput {
|
||||
r#type: "text",
|
||||
placeholder: "{id_placeholder}",
|
||||
value: "{id}",
|
||||
state: id_state,
|
||||
oninput: move |evt: FormEvent| data_ref.write().id = if evt.value.len() > 0 {
|
||||
Some(evt.value.clone()) } else { None },
|
||||
},
|
||||
},
|
||||
|
||||
div {
|
||||
class: ClassName::PASSWORD,
|
||||
TextField {
|
||||
id: "password",
|
||||
TextInput {
|
||||
r#type: "password",
|
||||
placeholder: "Password",
|
||||
value: "{password}",
|
||||
oninput: move |evt: FormEvent| login.write().password = Some(evt.value.clone()),
|
||||
state: password_state,
|
||||
oninput: move |evt: FormEvent| data_ref.write().password = if evt.value.len() > 0 {
|
||||
Some(evt.value.clone()) } else { None },
|
||||
},
|
||||
},
|
||||
|
||||
div {
|
||||
class: "{confirm_password_classes_str}",
|
||||
TextInput {
|
||||
r#type: "password",
|
||||
placeholder: "Confirm Password",
|
||||
value: "{confirm_password}",
|
||||
state: confirm_password_state,
|
||||
oninput: move |evt: FormEvent| data_ref.write().confirm_password = if evt.value.len() > 0 {
|
||||
Some(evt.value.clone()) } else { None },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -256,15 +542,13 @@ pub fn Login<'a>(cx: Scope<'a, LoginProps>) -> Element<'a> {
|
||||
div {
|
||||
class: ClassName::REGISTER,
|
||||
RegisterButton {
|
||||
id: "register",
|
||||
// TODO: Handle the registration process
|
||||
onclick: on_register,
|
||||
},
|
||||
},
|
||||
|
||||
div {
|
||||
class: ClassName::LOGIN,
|
||||
LoginButton {
|
||||
id: "login",
|
||||
focus: true,
|
||||
onclick: on_login,
|
||||
},
|
||||
|
@@ -45,16 +45,18 @@
|
||||
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-rows: $padding-row $profile-img-height auto 5% 5% 5% 5% 5% 0% 0% 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 . ."
|
||||
". . id id id id id id id . ."
|
||||
". . . . . . . . . . ."
|
||||
". . status status status status status status status . ."
|
||||
". . password password password password password password password . ."
|
||||
". . . . . . . . . . ."
|
||||
". . confirm confirm confirm confirm confirm confirm confirm . ."
|
||||
". . . . . . . . . . ."
|
||||
". . . . spinner spinner spinner . . . ."
|
||||
". . . . . . . . . . ."
|
||||
@@ -62,6 +64,12 @@
|
||||
". . . . . . . . . . ."
|
||||
;
|
||||
|
||||
transition: 300ms;
|
||||
|
||||
&.register {
|
||||
grid-template-rows: $padding-row $profile-img-height auto 5% 5% 5% 5% 5% 5% 5% 5% $spinner-height 5% $button-height $padding-row;
|
||||
}
|
||||
|
||||
.photo {
|
||||
grid-area: photo;
|
||||
|
||||
@@ -84,12 +92,21 @@
|
||||
grid-area: homeserver;
|
||||
}
|
||||
|
||||
.email {
|
||||
grid-area: username;
|
||||
.id {
|
||||
grid-area: id;
|
||||
}
|
||||
|
||||
.password {
|
||||
grid-area: status;
|
||||
grid-area: password;
|
||||
}
|
||||
|
||||
.confirm-password {
|
||||
grid-area: confirm;
|
||||
display: none;
|
||||
|
||||
&.show {
|
||||
display: initial;
|
||||
}
|
||||
}
|
||||
|
||||
.spinner {
|
||||
|
@@ -8,5 +8,5 @@ pub mod loading;
|
||||
pub mod login;
|
||||
pub mod main_window;
|
||||
pub mod spinner;
|
||||
pub mod text_field;
|
||||
pub mod text_input;
|
||||
pub mod wallpaper;
|
||||
|
@@ -1,45 +0,0 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
turf::style_sheet!("src/components/text_field.scss");
|
||||
|
||||
#[derive(Props)]
|
||||
pub struct TextFieldProps<'a> {
|
||||
id: Option<&'a str>,
|
||||
oninput: Option<EventHandler<'a, Event<FormData>>>,
|
||||
placeholder: Option<&'a str>,
|
||||
r#type: Option<&'a str>,
|
||||
value: Option<&'a str>,
|
||||
#[props(default = true)]
|
||||
is_value_valid: bool,
|
||||
}
|
||||
|
||||
pub fn TextField<'a>(cx: Scope<'a, TextFieldProps<'a>>) -> Element<'a> {
|
||||
let classes = [
|
||||
ClassName::ROOT,
|
||||
if !cx.props.is_value_valid {
|
||||
ClassName::INVALID_DATA
|
||||
} else {
|
||||
""
|
||||
},
|
||||
]
|
||||
.join(" ");
|
||||
|
||||
cx.render(rsx! {
|
||||
style { STYLE_SHEET },
|
||||
|
||||
input {
|
||||
class: "{classes}",
|
||||
|
||||
id: cx.props.id.unwrap_or(""),
|
||||
|
||||
oninput: move |evt| {
|
||||
if let Some(cb) = &cx.props.oninput {
|
||||
cb.call(evt);
|
||||
}
|
||||
},
|
||||
r#type: cx.props.r#type,
|
||||
placeholder: cx.props.placeholder,
|
||||
value: cx.props.value,
|
||||
}
|
||||
})
|
||||
}
|
@@ -1,25 +0,0 @@
|
||||
@import "../_base.scss"
|
||||
|
||||
.root {
|
||||
$horizontal-padding: 1vw;
|
||||
|
||||
padding-left: $horizontal-padding;
|
||||
padding-right: $horizontal-padding;
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
|
||||
height: calc(100% - (2 * ($border-normal-width)));
|
||||
width: calc(100% - (2 * ($border-normal-width + $horizontal-padding)));
|
||||
|
||||
margin: 0;
|
||||
|
||||
border: $border-normal;
|
||||
border-color: $color-primary-90;
|
||||
border-radius: $border-radius;
|
||||
|
||||
font-size: 2vh;
|
||||
|
||||
&.invalid-data {
|
||||
border-color: $color-critical;
|
||||
}
|
||||
}
|
88
src/components/text_input.rs
Normal file
88
src/components/text_input.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
turf::style_sheet!("src/components/text_input.scss");
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TextInputState {
|
||||
pub is_valid: bool,
|
||||
pub helper_text: Option<String>,
|
||||
}
|
||||
|
||||
impl TextInputState {
|
||||
pub fn new() -> Self {
|
||||
TextInputState {
|
||||
is_valid: true,
|
||||
helper_text: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.is_valid = true;
|
||||
self.helper_text = None;
|
||||
}
|
||||
|
||||
pub fn invalidate(&mut self, helper_text: String) {
|
||||
self.is_valid = false;
|
||||
self.helper_text = Some(helper_text);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Props)]
|
||||
pub struct TextInputProps<'a> {
|
||||
id: Option<&'a str>,
|
||||
r#type: Option<&'a str>,
|
||||
value: Option<&'a str>,
|
||||
placeholder: Option<&'a str>,
|
||||
oninput: Option<EventHandler<'a, Event<FormData>>>,
|
||||
state: Option<&'a UseRef<TextInputState>>,
|
||||
}
|
||||
|
||||
pub fn TextInput<'a>(cx: Scope<'a, TextInputProps<'a>>) -> Element<'a> {
|
||||
let mut level_class = "";
|
||||
let mut helper_text: String = "".to_string();
|
||||
|
||||
match cx.props.state {
|
||||
Some(state) => {
|
||||
if !state.read().is_valid {
|
||||
level_class = ClassName::INVALID;
|
||||
}
|
||||
if let Some(text) = &state.read().helper_text {
|
||||
helper_text = text.to_string();
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
||||
cx.render(rsx! {
|
||||
style { STYLE_SHEET },
|
||||
|
||||
div {
|
||||
class: ClassName::ROOT,
|
||||
|
||||
input {
|
||||
class: level_class,
|
||||
|
||||
id: cx.props.id.unwrap_or(""),
|
||||
|
||||
oninput: move |evt| {
|
||||
if let Some(cb) = &cx.props.oninput {
|
||||
cb.call(evt);
|
||||
}
|
||||
},
|
||||
r#type: cx.props.r#type,
|
||||
placeholder: cx.props.placeholder,
|
||||
value: cx.props.value,
|
||||
},
|
||||
|
||||
div {
|
||||
class: ClassName::HELPER_TEXT,
|
||||
|
||||
p {
|
||||
class: level_class,
|
||||
|
||||
helper_text
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
47
src/components/text_input.scss
Normal file
47
src/components/text_input.scss
Normal file
@@ -0,0 +1,47 @@
|
||||
@import "../_base.scss"
|
||||
|
||||
.root {
|
||||
height: 100%;
|
||||
|
||||
input {
|
||||
$horizontal-padding: 1vw;
|
||||
|
||||
padding-left: $horizontal-padding;
|
||||
padding-right: $horizontal-padding;
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
|
||||
height: calc(100% - (2 * ($border-normal-width)));
|
||||
width: calc(100% - (2 * ($border-normal-width + $horizontal-padding)));
|
||||
|
||||
margin: 0;
|
||||
|
||||
border: $border-normal;
|
||||
border-color: $color-primary-90;
|
||||
border-radius: $border-radius;
|
||||
|
||||
font-size: 2vh;
|
||||
|
||||
&.invalid {
|
||||
border-color: $color-critical;
|
||||
}
|
||||
}
|
||||
|
||||
.helper-text {
|
||||
margin: 0;
|
||||
margin-top: 0.3vh;
|
||||
|
||||
font-size: 1.2vh;
|
||||
|
||||
color: $color-primary-90;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
padding-left: 1vw;
|
||||
|
||||
&.invalid {
|
||||
color: $color-critical;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user