From 5c3e8c8e930123a504b53321618f2e3fe0e14d6b Mon Sep 17 00:00:00 2001 From: Adrian Wozniak Date: Tue, 14 Apr 2020 23:10:58 +0200 Subject: [PATCH] Finish time tracking, login page, validate token --- jirs-client/js/css/login.css | 38 +++++++++++++ jirs-client/js/css/variables.css | 74 +++++++++++++------------- jirs-client/js/styles.css | 1 + jirs-client/src/lib.rs | 43 ++++++++++++--- jirs-client/src/login.rs | 65 ++++++++++++++++++++-- jirs-client/src/modal/time_tracking.rs | 2 +- jirs-client/src/project.rs | 4 ++ jirs-client/src/project_settings.rs | 4 ++ jirs-client/src/shared/mod.rs | 31 +++++++---- jirs-client/src/shared/styled_icon.rs | 8 +-- jirs-client/src/ws/mod.rs | 8 +++ 11 files changed, 216 insertions(+), 62 deletions(-) create mode 100644 jirs-client/js/css/login.css diff --git a/jirs-client/js/css/login.css b/jirs-client/js/css/login.css new file mode 100644 index 00000000..8f3a9182 --- /dev/null +++ b/jirs-client/js/css/login.css @@ -0,0 +1,38 @@ +#login > .styledForm { + display: flex; + flex-direction: column; + margin: 124.5px auto 24px; + width: 400px; + /*padding: 32px 40px;*/ + 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); +} + +#login > .styledForm > .formElement > .formHeading { + color: var(--textMedium); + font-size: 1em; + line-height: 1.1428571428571428; + letter-spacing: -.003em; + font-family: var(--font-bold); + text-align: center; +} + +#login > .styledForm > .formElement > .noPasswordSection { + line-height: 32px; + margin-top: 15px; + display: flex; + cursor: pointer; +} + +#login > .styledForm > .formElement > .noPasswordSection > .styledIcon { + margin-right: 15px; + line-height: 32px; +} + +#login > .styledForm > .formElement > .noPasswordSection > span { + display: block; + line-height: 32px; +} diff --git a/jirs-client/js/css/variables.css b/jirs-client/js/css/variables.css index 100b4a9c..ff857920 100644 --- a/jirs-client/js/css/variables.css +++ b/jirs-client/js/css/variables.css @@ -1,44 +1,44 @@ :root { - --primary: #0052cc; /* Blue */ - --success: #0B875B; /* green */ - --danger: #E13C3C; /* red */ - --warning: #F89C1C; /* orange */ - --secondary: #F4F5F7; /* light grey */ - --textDarkest: #172b4d; - --textDark: #42526E; - --textMedium: #5E6C84; - --textLight: #8993a4; - --textLink: #0052cc; - --backgroundDarkPrimary: #0747A6; - --backgroundMedium: #dfe1e6; - --backgroundLight: #ebecf0; - --backgroundLightest: #F4F5F7; - --backgroundLightPrimary: #D2E5FE; - --backgroundLightSuccess: #E4FCEF; - --borderLightest: #dfe1e6; - --borderLight: #C1C7D0; - --borderInputFocus: #4c9aff; + --primary: rgb(0, 82, 204); /* Blue */ + --success: rgb(11, 135, 91); /* green */ + --danger: rgb(225, 60, 60); /* red */ + --warning: rgb(248, 156, 28); /* orange */ + --secondary: rgb(244, 245, 247); /* light grey */ + --textDarkest: rgb(23, 43, 77); + --textDark: rgb(66, 82, 110); + --textMedium: rgb(94, 108, 132); + --textLight: rgb(137, 147, 164); + --textLink: rgb(0, 82, 204); + --backgroundDarkPrimary: rgb(7, 71, 166); + --backgroundMedium: rgb(223, 225, 230); + --backgroundLight: rgb(235, 236, 240); + --backgroundLightest: rgb(244, 245, 247); + --backgroundLightPrimary: rgb(210, 229, 254); + --backgroundLightSuccess: rgb(228, 252, 239); + --borderLightest: rgb(223, 225, 230); + --borderLight: rgb(193, 199, 208); + --borderInputFocus: rgb(76, 154, 255); } :root { - --task: #4FADE6; /* blue */ - --bug: #E44D42; /* red */ - --story: #65BA43; /* green */ + --task: rgb(79, 173, 230); /* blue */ + --bug: rgb(228, 77, 66); /* red */ + --story: rgb(101, 186, 67); /* green */ } :root { - --highest: #CD1317; /* red */ - --high: #E9494A; /* orange */ - --medium: #E97F33; /* orange */ - --low: #2D8738; /* green */ - --lowest: #57A55A; /* green */ + --highest: rgb(205, 19, 23); /* red */ + --high: rgb(233, 73, 74); /* orange */ + --medium: rgb(233, 127, 51); /* orange */ + --low: rgb(45, 135, 56); /* green */ + --lowest: rgb(87, 165, 90); /* green */ } :root { --issue-color-backlog: var(--textDark); - --issue-color-inprogress: #fff; + --issue-color-inprogress: rgb(255, 255, 255); --issue-color-selected: var(--textDark); - --issue-color-done: #fff; + --issue-color-done: rgb(255, 255, 255); --issue-background-backlog: var(--backgroundMedium); --issue-background-inprogress: var(--primary); --issue-background-selected: var(--backgroundMedium); @@ -65,12 +65,12 @@ } :root { - --avatar-color-1: #DA7657; - --avatar-color-2: #6ADA57; - --avatar-color-3: #5784DA; - --avatar-color-4: #AA57DA; - --avatar-color-5: #DA5757; - --avatar-color-6: #DA5792; - --avatar-color-7: #57DACA; - --avatar-color-8: #57A5DA; + --avatar-color-1: rgb(218, 118, 87); + --avatar-color-2: rgb(106, 218, 87); + --avatar-color-3: rgb(87, 132, 218); + --avatar-color-4: rgb(170, 87, 218); + --avatar-color-5: rgb(218, 87, 87); + --avatar-color-6: rgb(218, 87, 146); + --avatar-color-7: rgb(87, 218, 202); + --avatar-color-8: rgb(87, 165, 218); } diff --git a/jirs-client/js/styles.css b/jirs-client/js/styles.css index 1e441490..48b25260 100644 --- a/jirs-client/js/styles.css +++ b/jirs-client/js/styles.css @@ -23,3 +23,4 @@ @import "./css/project.css"; @import "./css/projectSettings.css"; @import "./css/timeTracking.css"; +@import "./css/login.css"; diff --git a/jirs-client/src/lib.rs b/jirs-client/src/lib.rs index 501f23f3..98176d9b 100644 --- a/jirs-client/src/lib.rs +++ b/jirs-client/src/lib.rs @@ -57,8 +57,14 @@ pub enum ProjectSettingsFieldId { Category, } +#[derive(Clone, Debug, PartialOrd, PartialEq, Hash)] +pub enum LoginFieldId { + Email, +} + #[derive(Clone, Debug, PartialOrd, PartialEq, Hash)] pub enum FieldId { + Login(LoginFieldId), // issue AddIssueModal(AddIssueModalFieldId), EditIssueModal(EditIssueModalFieldId), @@ -101,6 +107,9 @@ impl std::fmt::Display for FieldId { ProjectSettingsFieldId::Description => f.write_str("projectSettings-description"), ProjectSettingsFieldId::Category => f.write_str("projectSettings-category"), }, + FieldId::Login(sub) => match sub { + LoginFieldId::Email => f.write_str("login-email"), + }, } } } @@ -175,6 +184,14 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders) { log!(msg); } match &msg { + Msg::AuthTokenStored => { + seed::push_route(vec!["/dashboard"]); + return; + } + Msg::AuthTokenErased => { + seed::push_route(vec!["/login"]); + return; + } Msg::ChangePage(page) => { model.page = *page; } @@ -249,9 +266,7 @@ pub fn handle_ws_message(value: &wasm_bindgen::JsValue) { #[wasm_bindgen] pub fn reconnected() { - if let Ok(uuid) = read_auth_token() { - send_ws_msg(WsMsg::AuthorizeRequest(uuid)); - } + authorize_or_redirect(); } #[wasm_bindgen] @@ -296,12 +311,28 @@ pub fn render() { .routes(routes) .build_and_start(); - if let Ok(uuid) = crate::shared::read_auth_token() { - send_ws_msg(WsMsg::AuthorizeRequest(uuid)); - }; + authorize_or_redirect(); let cell_app = std::sync::RwLock::new(app); unsafe { APP = Some(cell_app); }; } + +#[inline] +fn authorize_or_redirect() { + match crate::shared::read_auth_token() { + Ok(uuid) => { + send_ws_msg(WsMsg::AuthorizeRequest(uuid)); + } + Err(..) => { + let pathname = seed::document().location().unwrap().pathname().unwrap(); + match pathname.as_str() { + "/login" | "/register" => {} + _ => { + seed::push_route(vec!["login"]); + } + }; + } + }; +} diff --git a/jirs-client/src/login.rs b/jirs-client/src/login.rs index 4d4c4eaa..e5011a8f 100644 --- a/jirs-client/src/login.rs +++ b/jirs-client/src/login.rs @@ -1,8 +1,65 @@ -use crate::{model, Msg}; use seed::{prelude::*, *}; -pub fn update(_msg: Msg, _model: &mut model::Model, _orders: &mut impl Orders) {} +use crate::model::Page; +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::{outer_layout, ToNode}; +use crate::{model, FieldId, LoginFieldId, Msg}; -pub fn view(_model: &model::Model) -> Node { - div![] +pub fn update(msg: Msg, model: &mut model::Model, _orders: &mut impl Orders) { + if model.page != Page::Login { + return; + } + + match msg { + _ => (), + }; +} + +pub fn view(model: &model::Model) -> Node { + let login = StyledInput::build(FieldId::Login(LoginFieldId::Email)) + .valid(true) + .build() + .into_node(); + let login_field = StyledField::build() + .label("Login") + .input(login) + .build() + .into_node(); + + let email = StyledInput::build(FieldId::Login(LoginFieldId::Email)) + .valid(true) + .build() + .into_node(); + let email_field = StyledField::build() + .label("E-Mail") + .input(email) + .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 form = StyledForm::build() + .heading("Sign In to your account") + .add_field(login_field) + .add_field(email_field) + .add_field(no_pass_section) + .build() + .into_node(); + + let children = vec![form]; + outer_layout(model, "login", children) } diff --git a/jirs-client/src/modal/time_tracking.rs b/jirs-client/src/modal/time_tracking.rs index e193b429..129eed5e 100644 --- a/jirs-client/src/modal/time_tracking.rs +++ b/jirs-client/src/modal/time_tracking.rs @@ -9,7 +9,7 @@ use crate::shared::styled_input::StyledInput; use crate::shared::styled_modal::StyledModal; use crate::shared::tracking_widget::tracking_widget; use crate::shared::{find_issue, ToNode}; -use crate::{EditIssueModalFieldId, FieldId, Msg, ProjectSettingsFieldId}; +use crate::{EditIssueModalFieldId, FieldId, Msg}; pub fn view(model: &Model, issue_id: IssueId) -> Node { let _issue = match find_issue(model, issue_id) { diff --git a/jirs-client/src/project.rs b/jirs-client/src/project.rs index ed1f55cf..e9e625cc 100644 --- a/jirs-client/src/project.rs +++ b/jirs-client/src/project.rs @@ -14,6 +14,10 @@ use crate::shared::{drag_ev, inner_layout, ToNode}; use crate::{EditIssueModalFieldId, FieldId, Msg}; pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Orders) { + if model.user.is_none() { + return; + } + match &model.page { Page::Project | Page::AddIssue | Page::EditIssue(..) => { model.page_content = PageContent::Project(ProjectPage::default()); diff --git a/jirs-client/src/project_settings.rs b/jirs-client/src/project_settings.rs index 19303859..6eb456a1 100644 --- a/jirs-client/src/project_settings.rs +++ b/jirs-client/src/project_settings.rs @@ -16,6 +16,10 @@ use crate::FieldChange::TabChanged; use crate::{model, FieldId, Msg, ProjectSettingsFieldId}; pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders) { + if model.user.is_none() { + return; + } + if model.page != Page::ProjectSettings { log!("not settings page"); return; diff --git a/jirs-client/src/shared/mod.rs b/jirs-client/src/shared/mod.rs index 657c5940..b19bc425 100644 --- a/jirs-client/src/shared/mod.rs +++ b/jirs-client/src/shared/mod.rs @@ -51,22 +51,33 @@ pub fn inner_layout( ] } +pub fn outer_layout(_model: &Model, page_name: &str, children: Vec>) -> Node { + article![ + class!["outer-layout", "outerPage"], + id![page_name], + children + ] +} + pub fn write_auth_token(token: Option) -> Result { let w = window(); let store = match w.local_storage() { Ok(Some(store)) => store, _ => return Err("Local storage is not available".to_string()), }; - store - .set_item( - "authToken", - token - .as_ref() - .map(|t| format!("{}", t)) - .unwrap_or_default() - .as_str(), - ) - .map_err(|_e| "Failed to read auth token".to_string())?; + match token { + Some(token) => { + store + .set_item("authToken", format!("{}", token).as_str()) + .map_err(|_e| "Failed to read auth token".to_string())?; + } + _ => { + store + .remove_item("authToken") + .map_err(|_e| "Failed to read auth token".to_string())?; + } + } + Ok(match token { Some(_) => Msg::AuthTokenStored, _ => Msg::AuthTokenErased, diff --git a/jirs-client/src/shared/styled_icon.rs b/jirs-client/src/shared/styled_icon.rs index ab5639de..70f90765 100644 --- a/jirs-client/src/shared/styled_icon.rs +++ b/jirs-client/src/shared/styled_icon.rs @@ -170,10 +170,10 @@ impl StyledIconBuilder { } pub struct StyledIcon { - pub icon: Icon, - pub size: Option, - pub class_list: Vec, - pub style_list: Vec, + icon: Icon, + size: Option, + class_list: Vec, + style_list: Vec, } impl StyledIcon { diff --git a/jirs-client/src/ws/mod.rs b/jirs-client/src/ws/mod.rs index 955bda86..47f4ec41 100644 --- a/jirs-client/src/ws/mod.rs +++ b/jirs-client/src/ws/mod.rs @@ -2,6 +2,7 @@ use seed::prelude::*; use jirs_data::WsMsg; +use crate::shared::write_auth_token; use crate::{model, Msg, APP}; pub mod issue; @@ -24,6 +25,13 @@ pub fn update(msg: &Msg, model: &mut model::Model, orders: &mut impl Orders Msg::WsMsg(WsMsg::AuthorizeLoaded(Ok(user))) => { model.user = Some(user.clone()); } + Msg::WsMsg(WsMsg::AuthorizeExpired) => { + use seed::*; + error!("Session expired"); + if let Ok(msg) = write_auth_token(None) { + orders.skip().send_msg(msg); + } + } // project Msg::WsMsg(WsMsg::ProjectLoaded(project)) => { model.project = Some(project.clone());