Add and style register view

This commit is contained in:
Adrian Woźniak 2020-04-17 16:28:02 +02:00
parent b22210d55a
commit 0e2cc62c30
13 changed files with 363 additions and 68 deletions

View File

@ -52,3 +52,8 @@
display: block; display: block;
line-height: 32px; line-height: 32px;
} }
#login .twoRow {
display: flex;
justify-content: space-between;
}

View File

@ -0,0 +1,37 @@
#register > .styledForm {
display: flex;
flex-direction: column;
margin: 0 auto 24px;
width: 400px;
background: rgb(255, 255, 255) none repeat scroll 0 0;
border-radius: 3px;
box-shadow: rgba(0, 0, 0, 0.1) 0 0 10px;
box-sizing: border-box;
color: var(--textMedium);
}
#register > .styledForm:first-of-type {
margin-top: 124.5px;
}
#register .twoRow {
display: flex;
justify-content: space-between;
}
#register > .styledForm > .formElement > .noPasswordSection {
line-height: 32px;
margin-top: 15px;
display: flex;
cursor: pointer;
}
#register > .styledForm > .formElement > .noPasswordSection > .styledIcon {
margin-right: 5px;
line-height: 32px;
}
#register > .styledForm > .formElement > .noPasswordSection > span {
display: block;
line-height: 32px;
}

View File

@ -0,0 +1,11 @@
.styledLink {
color: var(--textDark);
font-family: var(--font-medium);
height: 32px;
vertical-align: middle;
line-height: 2;
appearance: none;
cursor: pointer;
user-select: none;
font-size: 14.5px;
}

View File

@ -18,9 +18,11 @@
@import "./css/styledEditor.css"; @import "./css/styledEditor.css";
@import "./css/styledComment.css"; @import "./css/styledComment.css";
@import "./css/styledPage.css"; @import "./css/styledPage.css";
@import "./css/styledLink.css";
@import "./css/app.css"; @import "./css/app.css";
@import "./css/issue.css"; @import "./css/issue.css";
@import "./css/project.css"; @import "./css/project.css";
@import "./css/projectSettings.css"; @import "./css/projectSettings.css";
@import "./css/timeTracking.css"; @import "./css/timeTracking.css";
@import "./css/login.css"; @import "./css/login.css";
@import "./css/register.css";

View File

@ -10,13 +10,14 @@ use crate::shared::styled_editor::Mode as TabMode;
use crate::shared::styled_select::StyledSelectChange; use crate::shared::styled_select::StyledSelectChange;
mod api; mod api;
mod login;
mod modal; mod modal;
mod model; mod model;
mod project; mod project;
mod project_settings; mod project_settings;
mod register;
mod shared; mod shared;
mod sign_in;
mod sign_up;
mod validations;
mod ws; mod ws;
pub type AvatarFilterActive = bool; pub type AvatarFilterActive = bool;
@ -30,7 +31,8 @@ pub enum EditIssueModalSection {
#[derive(Clone, Debug, PartialOrd, PartialEq, Hash)] #[derive(Clone, Debug, PartialOrd, PartialEq, Hash)]
pub enum FieldId { pub enum FieldId {
Login(LoginFieldId), SignIn(SignInFieldId),
SignUp(SignUpFieldId),
// issue // issue
AddIssueModal(IssueFieldId), AddIssueModal(IssueFieldId),
EditIssueModal(EditIssueModalSection), EditIssueModal(EditIssueModalSection),
@ -103,10 +105,14 @@ impl std::fmt::Display for FieldId {
ProjectFieldId::Description => f.write_str("projectSettings-description"), ProjectFieldId::Description => f.write_str("projectSettings-description"),
ProjectFieldId::Category => f.write_str("projectSettings-category"), ProjectFieldId::Category => f.write_str("projectSettings-category"),
}, },
FieldId::Login(sub) => match sub { FieldId::SignIn(sub) => match sub {
LoginFieldId::Email => f.write_str("login-email"), SignInFieldId::Email => f.write_str("login-email"),
LoginFieldId::Username => f.write_str("login-username"), SignInFieldId::Username => f.write_str("login-username"),
LoginFieldId::Token => f.write_str("login-token"), SignInFieldId::Token => f.write_str("login-token"),
},
FieldId::SignUp(sub) => match sub {
SignUpFieldId::Username => f.write_str("signUp-email"),
SignUpFieldId::Email => f.write_str("signUp-username"),
}, },
} }
} }
@ -193,7 +199,7 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
} }
Msg::AuthTokenErased => { Msg::AuthTokenErased => {
seed::push_route(vec!["login"]); seed::push_route(vec!["login"]);
orders.skip().send_msg(Msg::ChangePage(Page::Login)); orders.skip().send_msg(Msg::ChangePage(Page::SignIn));
authorize_or_redirect(); authorize_or_redirect();
return; return;
} }
@ -210,8 +216,8 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
match model.page { match model.page {
Page::Project | Page::AddIssue | Page::EditIssue(..) => project::update(msg, model, orders), Page::Project | Page::AddIssue | Page::EditIssue(..) => project::update(msg, model, orders),
Page::ProjectSettings => project_settings::update(msg, model, orders), Page::ProjectSettings => project_settings::update(msg, model, orders),
Page::Login => login::update(msg, model, orders), Page::SignIn => sign_in::update(msg, model, orders),
Page::Register => register::update(msg, model, orders), Page::SignUp => sign_up::update(msg, model, orders),
} }
if cfg!(debug_assertions) { if cfg!(debug_assertions) {
// debug!(model); // debug!(model);
@ -223,8 +229,8 @@ fn view(model: &model::Model) -> Node<Msg> {
Page::Project | Page::AddIssue => project::view(model), Page::Project | Page::AddIssue => project::view(model),
Page::EditIssue(_id) => project::view(model), Page::EditIssue(_id) => project::view(model),
Page::ProjectSettings => project_settings::view(model), Page::ProjectSettings => project_settings::view(model),
Page::Login => login::view(model), Page::SignIn => sign_in::view(model),
Page::Register => register::view(model), Page::SignUp => sign_up::view(model),
} }
} }
@ -241,8 +247,8 @@ fn routes(url: Url) -> Option<Msg> {
}, },
"add-issue" => Some(Msg::ChangePage(Page::AddIssue)), "add-issue" => Some(Msg::ChangePage(Page::AddIssue)),
"project-settings" => Some(Msg::ChangePage(model::Page::ProjectSettings)), "project-settings" => Some(Msg::ChangePage(model::Page::ProjectSettings)),
"login" => Some(Msg::ChangePage(model::Page::Login)), "login" => Some(Msg::ChangePage(model::Page::SignIn)),
"register" => Some(Msg::ChangePage(model::Page::Register)), "register" => Some(Msg::ChangePage(model::Page::SignUp)),
_ => Some(Msg::ChangePage(model::Page::Project)), _ => Some(Msg::ChangePage(model::Page::Project)),
} }
} }

View File

@ -140,8 +140,8 @@ pub enum Page {
EditIssue(IssueId), EditIssue(IssueId),
AddIssue, AddIssue,
ProjectSettings, ProjectSettings,
Login, SignIn,
Register, SignUp,
} }
impl Page { impl Page {
@ -151,8 +151,8 @@ impl Page {
Page::EditIssue(id) => format!("/issues/{id}", id = id), Page::EditIssue(id) => format!("/issues/{id}", id = id),
Page::AddIssue => "/add-issues".to_string(), Page::AddIssue => "/add-issues".to_string(),
Page::ProjectSettings => "/project-settings".to_string(), Page::ProjectSettings => "/project-settings".to_string(),
Page::Login => "/login".to_string(), Page::SignIn => "/login".to_string(),
Page::Register => "/register".to_string(), Page::SignUp => "/register".to_string(),
} }
} }
} }
@ -219,7 +219,7 @@ impl ProjectSettingsPage {
} }
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct LoginPage { pub struct SignInPage {
pub username: String, pub username: String,
pub email: String, pub email: String,
pub token: String, pub token: String,
@ -231,9 +231,20 @@ pub struct LoginPage {
pub token_touched: bool, pub token_touched: bool,
} }
#[derive(Debug, Default)]
pub struct SignUpPage {
pub username: String,
pub email: String,
pub sign_up_success: bool,
// touched
pub username_touched: bool,
pub email_touched: bool,
}
#[derive(Debug)] #[derive(Debug)]
pub enum PageContent { pub enum PageContent {
Login(LoginPage), SignIn(SignInPage),
SignUp(SignUpPage),
Project(ProjectPage), Project(ProjectPage),
ProjectSettings(ProjectSettingsPage), ProjectSettings(ProjectSettingsPage),
} }

View File

@ -1,8 +0,0 @@
use crate::{model, Msg};
use seed::{prelude::*, *};
pub fn update(_msg: Msg, _model: &mut model::Model, _orders: &mut impl Orders<Msg>) {}
pub fn view(_model: &model::Model) -> Node<Msg> {
div![]
}

View File

@ -16,6 +16,7 @@ pub mod styled_field;
pub mod styled_form; pub mod styled_form;
pub mod styled_icon; pub mod styled_icon;
pub mod styled_input; pub mod styled_input;
pub mod styled_link;
pub mod styled_modal; pub mod styled_modal;
pub mod styled_select; pub mod styled_select;
pub mod styled_select_child; pub mod styled_select_child;

View File

@ -0,0 +1,85 @@
use seed::{prelude::*, *};
use crate::shared::ToNode;
use crate::Msg;
pub struct StyledLink {
children: Vec<Node<Msg>>,
class_list: Vec<String>,
href: String,
}
impl StyledLink {
pub fn build() -> StyledLinkBuilder {
StyledLinkBuilder::default()
}
}
#[derive(Default)]
pub struct StyledLinkBuilder {
children: Vec<Node<Msg>>,
class_list: Vec<String>,
href: String,
}
impl StyledLinkBuilder {
pub fn add_child(mut self, child: Node<Msg>) -> Self {
self.children.push(child);
self
}
pub fn add_class<S>(mut self, name: S) -> Self
where
S: Into<String>,
{
self.class_list.push(name.into());
self
}
pub fn href<S>(mut self, href: S) -> Self
where
S: Into<String>,
{
self.href = href.into();
self
}
pub fn text<S>(mut self, s: S) -> Self
where
S: Into<String>,
{
let text: String = s.into();
self.children.push(span![text]);
self
}
pub fn build(self) -> StyledLink {
StyledLink {
children: self.children,
class_list: self.class_list,
href: self.href,
}
}
}
impl ToNode for StyledLink {
fn into_node(self) -> Node<Msg> {
render(self)
}
}
pub fn render(values: StyledLink) -> Node<Msg> {
let StyledLink {
children,
mut class_list,
href,
} = values;
class_list.push("styledLink".to_string());
a![
attrs![
At::Class => class_list.join(" "),
At::Href => href,
],
children,
]
}

View File

@ -6,40 +6,42 @@ use uuid::Uuid;
use jirs_data::WsMsg; use jirs_data::WsMsg;
use crate::api::send_ws_msg; use crate::api::send_ws_msg;
use crate::model::{LoginPage, Page, PageContent}; use crate::model::{Page, PageContent, SignInPage};
use crate::shared::styled_button::StyledButton; use crate::shared::styled_button::StyledButton;
use crate::shared::styled_field::StyledField; use crate::shared::styled_field::StyledField;
use crate::shared::styled_form::StyledForm; use crate::shared::styled_form::StyledForm;
use crate::shared::styled_icon::{Icon, StyledIcon}; use crate::shared::styled_icon::{Icon, StyledIcon};
use crate::shared::styled_input::StyledInput; use crate::shared::styled_input::StyledInput;
use crate::shared::styled_link::StyledLink;
use crate::shared::{outer_layout, write_auth_token, ToNode}; use crate::shared::{outer_layout, write_auth_token, ToNode};
use crate::{model, FieldId, LoginFieldId, Msg}; use crate::validations::{is_email, is_token};
use crate::{model, FieldId, Msg, SignInFieldId};
pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) { pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
if model.page != Page::Login { if model.page != Page::SignIn {
return; return;
} }
if msg == Msg::ChangePage(Page::Login) { if msg == Msg::ChangePage(Page::SignIn) {
model.page_content = PageContent::Login(LoginPage::default()); model.page_content = PageContent::SignIn(SignInPage::default());
return; return;
} }
let page = match &mut model.page_content { let page = match &mut model.page_content {
PageContent::Login(page) => page, PageContent::SignIn(page) => page,
_ => return, _ => return,
}; };
match msg { match msg {
Msg::InputChanged(FieldId::Login(LoginFieldId::Username), value) => { Msg::InputChanged(FieldId::SignIn(SignInFieldId::Username), value) => {
page.username = value; page.username = value;
page.username_touched = true; page.username_touched = true;
} }
Msg::InputChanged(FieldId::Login(LoginFieldId::Email), value) => { Msg::InputChanged(FieldId::SignIn(SignInFieldId::Email), value) => {
page.email = value; page.email = value;
page.email_touched = true; page.email_touched = true;
} }
Msg::InputChanged(FieldId::Login(LoginFieldId::Token), value) => { Msg::InputChanged(FieldId::SignIn(SignInFieldId::Token), value) => {
page.token = value; page.token = value;
page.token_touched = true; page.token_touched = true;
} }
@ -78,11 +80,11 @@ pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>)
pub fn view(model: &model::Model) -> Node<Msg> { pub fn view(model: &model::Model) -> Node<Msg> {
let page = match &model.page_content { let page = match &model.page_content {
PageContent::Login(page) => page, PageContent::SignIn(page) => page,
_ => return empty![], _ => return empty![],
}; };
let username = StyledInput::build(FieldId::Login(LoginFieldId::Username)) let username = StyledInput::build(FieldId::SignIn(SignInFieldId::Username))
.value(page.username.as_str()) .value(page.username.as_str())
.valid(!page.username_touched || page.username.len() > 1) .valid(!page.username_touched || page.username.len() > 1)
.build() .build()
@ -93,7 +95,7 @@ pub fn view(model: &model::Model) -> Node<Msg> {
.build() .build()
.into_node(); .into_node();
let email = StyledInput::build(FieldId::Login(LoginFieldId::Email)) let email = StyledInput::build(FieldId::SignIn(SignInFieldId::Email))
.value(page.email.as_str()) .value(page.email.as_str())
.valid(!page.email_touched || is_email(page.email.as_str())) .valid(!page.email_touched || is_email(page.email.as_str()))
.build() .build()
@ -116,7 +118,15 @@ pub fn view(model: &model::Model) -> Node<Msg> {
} }
.build() .build()
.into_node(); .into_node();
let submit_field = StyledField::build().input(submit).build().into_node(); let register_link = StyledLink::build()
.text("Register")
.href("/register")
.build()
.into_node();
let submit_field = StyledField::build()
.input(div![class!["twoRow"], submit, register_link,])
.build()
.into_node();
let help_icon = StyledIcon::build(Icon::Help) let help_icon = StyledIcon::build(Icon::Help)
.add_class("noPasswordHelp") .add_class("noPasswordHelp")
@ -145,7 +155,7 @@ pub fn view(model: &model::Model) -> Node<Msg> {
.build() .build()
.into_node(); .into_node();
let token = StyledInput::build(FieldId::Login(LoginFieldId::Token)) let token = StyledInput::build(FieldId::SignIn(SignInFieldId::Token))
.value(page.token.as_str()) .value(page.token.as_str())
.valid(!page.token_touched || is_token(page.token.as_str())) .valid(!page.token_touched || is_token(page.token.as_str()))
.build() .build()
@ -177,28 +187,3 @@ pub fn view(model: &model::Model) -> Node<Msg> {
let children = vec![sign_in_form, bind_token_form]; let children = vec![sign_in_form, bind_token_form];
outer_layout(model, "login", children) outer_layout(model, "login", children)
} }
fn is_token(s: &str) -> bool {
uuid::Uuid::from_str(s).is_ok()
}
fn is_email(s: &str) -> bool {
let mut has_at = false;
let mut has_dot = false;
for c in s.chars() {
match c {
'\n' | ' ' | '\t' | '\r' => return false,
'@' if !has_at => {
has_at = true;
}
'@' if has_at => return false,
'.' if has_at => {
has_dot = true;
}
_ if has_dot => return true,
_ => (),
}
}
return false;
}

127
jirs-client/src/sign_up.rs Normal file
View File

@ -0,0 +1,127 @@
use seed::{prelude::*, *};
use jirs_data::{SignUpFieldId, WsMsg};
use crate::model::{Page, PageContent, SignUpPage};
use crate::shared::styled_button::StyledButton;
use crate::shared::styled_field::StyledField;
use crate::shared::styled_form::StyledForm;
use crate::shared::styled_icon::{Icon, StyledIcon};
use crate::shared::styled_input::StyledInput;
use crate::shared::styled_link::StyledLink;
use crate::shared::{outer_layout, ToNode};
use crate::validations::is_email;
use crate::{model, FieldId, Msg};
pub fn update(msg: Msg, model: &mut model::Model, _orders: &mut impl Orders<Msg>) {
if model.page != Page::SignUp {
return;
}
if msg == Msg::ChangePage(Page::SignUp) {
model.page_content = PageContent::SignUp(SignUpPage::default());
return;
}
let page = match &mut model.page_content {
PageContent::SignUp(page) => page,
_ => return,
};
match msg {
Msg::InputChanged(FieldId::SignUp(SignUpFieldId::Username), value) => {
page.username = value;
page.username_touched = true;
}
Msg::InputChanged(FieldId::SignUp(SignUpFieldId::Email), value) => {
page.email = value;
page.email_touched = true;
}
Msg::WsMsg(WsMsg::SignUpSuccess) => {
page.sign_up_success = true;
}
_ => (),
}
}
pub fn view(model: &model::Model) -> Node<Msg> {
let page = match &model.page_content {
PageContent::SignUp(page) => page,
_ => return empty![],
};
let username = StyledInput::build(FieldId::SignUp(SignUpFieldId::Username))
.value(page.username.as_str())
.valid(!page.username_touched || page.username.len() > 1)
.build()
.into_node();
let username_field = StyledField::build()
.label("Username")
.input(username)
.build()
.into_node();
let email = StyledInput::build(FieldId::SignUp(SignUpFieldId::Email))
.value(page.email.as_str())
.valid(!page.email_touched || is_email(page.email.as_str()))
.build()
.into_node();
let email_field = StyledField::build()
.label("E-Mail")
.input(email)
.build()
.into_node();
let submit = if page.sign_up_success {
StyledButton::build()
.success()
.text("✓ Please check your mail")
} else {
StyledButton::build()
.primary()
.text("Register")
.on_click(mouse_ev(Ev::Click, |_| Msg::SignInRequest))
}
.build()
.into_node();
let sign_in_link = StyledLink::build()
.text("Sign In")
.href("/login")
.build()
.into_node();
let submit_field = StyledField::build()
.input(div![class!["twoRow"], submit, sign_in_link,])
.build()
.into_node();
let help_icon = StyledIcon::build(Icon::Help)
.add_class("noPasswordHelp")
.size(22)
.build()
.into_node();
let no_pass_section = div![
class!["noPasswordSection"],
attrs![At::Title => "We don't believe password is helping anyone. Instead after user provide correct login and e-mail he'll receive mail with 1-use token."],
help_icon,
span!["Why I don't see password?"]
];
let sign_up_form = StyledForm::build()
.heading("Sign In to your account")
.on_submit(ev(Ev::Submit, |ev| {
ev.stop_propagation();
ev.prevent_default();
Msg::SignInRequest
}))
.add_field(username_field)
.add_field(email_field)
.add_field(submit_field)
.add_field(no_pass_section)
.build()
.into_node();
let children = vec![sign_up_form];
outer_layout(model, "register", children)
}

View File

@ -0,0 +1,26 @@
use std::str::FromStr;
pub fn is_email(s: &str) -> bool {
let mut has_at = false;
let mut has_dot = false;
for c in s.chars() {
match c {
'\n' | ' ' | '\t' | '\r' => return false,
'@' if !has_at => {
has_at = true;
}
'@' if has_at => return false,
'.' if has_at => {
has_dot = true;
}
_ if has_dot => return true,
_ => (),
}
}
return false;
}
pub fn is_token(s: &str) -> bool {
uuid::Uuid::from_str(s).is_ok()
}

View File

@ -484,12 +484,18 @@ pub enum ProjectFieldId {
} }
#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, PartialEq, Hash)] #[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, PartialEq, Hash)]
pub enum LoginFieldId { pub enum SignInFieldId {
Username, Username,
Email, Email,
Token, Token,
} }
#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, PartialEq, Hash)]
pub enum SignUpFieldId {
Username,
Email,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, PartialEq, Hash)] #[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, PartialEq, Hash)]
pub enum CommentFieldId { pub enum CommentFieldId {
Body, Body,
@ -524,6 +530,7 @@ pub enum WsMsg {
BindTokenCheck(Uuid), BindTokenCheck(Uuid),
BindTokenBad, BindTokenBad,
BindTokenOk(Uuid), BindTokenOk(Uuid),
SignUpSuccess,
// project page // project page
ProjectRequest, ProjectRequest,