Finish time tracking, login page, validate token

This commit is contained in:
Adrian Wozniak 2020-04-14 23:10:58 +02:00
parent 350337f0ed
commit 5c3e8c8e93
11 changed files with 216 additions and 62 deletions

View File

@ -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;
}

View File

@ -1,44 +1,44 @@
:root { :root {
--primary: #0052cc; /* Blue */ --primary: rgb(0, 82, 204); /* Blue */
--success: #0B875B; /* green */ --success: rgb(11, 135, 91); /* green */
--danger: #E13C3C; /* red */ --danger: rgb(225, 60, 60); /* red */
--warning: #F89C1C; /* orange */ --warning: rgb(248, 156, 28); /* orange */
--secondary: #F4F5F7; /* light grey */ --secondary: rgb(244, 245, 247); /* light grey */
--textDarkest: #172b4d; --textDarkest: rgb(23, 43, 77);
--textDark: #42526E; --textDark: rgb(66, 82, 110);
--textMedium: #5E6C84; --textMedium: rgb(94, 108, 132);
--textLight: #8993a4; --textLight: rgb(137, 147, 164);
--textLink: #0052cc; --textLink: rgb(0, 82, 204);
--backgroundDarkPrimary: #0747A6; --backgroundDarkPrimary: rgb(7, 71, 166);
--backgroundMedium: #dfe1e6; --backgroundMedium: rgb(223, 225, 230);
--backgroundLight: #ebecf0; --backgroundLight: rgb(235, 236, 240);
--backgroundLightest: #F4F5F7; --backgroundLightest: rgb(244, 245, 247);
--backgroundLightPrimary: #D2E5FE; --backgroundLightPrimary: rgb(210, 229, 254);
--backgroundLightSuccess: #E4FCEF; --backgroundLightSuccess: rgb(228, 252, 239);
--borderLightest: #dfe1e6; --borderLightest: rgb(223, 225, 230);
--borderLight: #C1C7D0; --borderLight: rgb(193, 199, 208);
--borderInputFocus: #4c9aff; --borderInputFocus: rgb(76, 154, 255);
} }
:root { :root {
--task: #4FADE6; /* blue */ --task: rgb(79, 173, 230); /* blue */
--bug: #E44D42; /* red */ --bug: rgb(228, 77, 66); /* red */
--story: #65BA43; /* green */ --story: rgb(101, 186, 67); /* green */
} }
:root { :root {
--highest: #CD1317; /* red */ --highest: rgb(205, 19, 23); /* red */
--high: #E9494A; /* orange */ --high: rgb(233, 73, 74); /* orange */
--medium: #E97F33; /* orange */ --medium: rgb(233, 127, 51); /* orange */
--low: #2D8738; /* green */ --low: rgb(45, 135, 56); /* green */
--lowest: #57A55A; /* green */ --lowest: rgb(87, 165, 90); /* green */
} }
:root { :root {
--issue-color-backlog: var(--textDark); --issue-color-backlog: var(--textDark);
--issue-color-inprogress: #fff; --issue-color-inprogress: rgb(255, 255, 255);
--issue-color-selected: var(--textDark); --issue-color-selected: var(--textDark);
--issue-color-done: #fff; --issue-color-done: rgb(255, 255, 255);
--issue-background-backlog: var(--backgroundMedium); --issue-background-backlog: var(--backgroundMedium);
--issue-background-inprogress: var(--primary); --issue-background-inprogress: var(--primary);
--issue-background-selected: var(--backgroundMedium); --issue-background-selected: var(--backgroundMedium);
@ -65,12 +65,12 @@
} }
:root { :root {
--avatar-color-1: #DA7657; --avatar-color-1: rgb(218, 118, 87);
--avatar-color-2: #6ADA57; --avatar-color-2: rgb(106, 218, 87);
--avatar-color-3: #5784DA; --avatar-color-3: rgb(87, 132, 218);
--avatar-color-4: #AA57DA; --avatar-color-4: rgb(170, 87, 218);
--avatar-color-5: #DA5757; --avatar-color-5: rgb(218, 87, 87);
--avatar-color-6: #DA5792; --avatar-color-6: rgb(218, 87, 146);
--avatar-color-7: #57DACA; --avatar-color-7: rgb(87, 218, 202);
--avatar-color-8: #57A5DA; --avatar-color-8: rgb(87, 165, 218);
} }

View File

@ -23,3 +23,4 @@
@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";

View File

@ -57,8 +57,14 @@ pub enum ProjectSettingsFieldId {
Category, Category,
} }
#[derive(Clone, Debug, PartialOrd, PartialEq, Hash)]
pub enum LoginFieldId {
Email,
}
#[derive(Clone, Debug, PartialOrd, PartialEq, Hash)] #[derive(Clone, Debug, PartialOrd, PartialEq, Hash)]
pub enum FieldId { pub enum FieldId {
Login(LoginFieldId),
// issue // issue
AddIssueModal(AddIssueModalFieldId), AddIssueModal(AddIssueModalFieldId),
EditIssueModal(EditIssueModalFieldId), EditIssueModal(EditIssueModalFieldId),
@ -101,6 +107,9 @@ impl std::fmt::Display for FieldId {
ProjectSettingsFieldId::Description => f.write_str("projectSettings-description"), ProjectSettingsFieldId::Description => f.write_str("projectSettings-description"),
ProjectSettingsFieldId::Category => f.write_str("projectSettings-category"), 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<Msg>) {
log!(msg); log!(msg);
} }
match &msg { match &msg {
Msg::AuthTokenStored => {
seed::push_route(vec!["/dashboard"]);
return;
}
Msg::AuthTokenErased => {
seed::push_route(vec!["/login"]);
return;
}
Msg::ChangePage(page) => { Msg::ChangePage(page) => {
model.page = *page; model.page = *page;
} }
@ -249,9 +266,7 @@ pub fn handle_ws_message(value: &wasm_bindgen::JsValue) {
#[wasm_bindgen] #[wasm_bindgen]
pub fn reconnected() { pub fn reconnected() {
if let Ok(uuid) = read_auth_token() { authorize_or_redirect();
send_ws_msg(WsMsg::AuthorizeRequest(uuid));
}
} }
#[wasm_bindgen] #[wasm_bindgen]
@ -296,12 +311,28 @@ pub fn render() {
.routes(routes) .routes(routes)
.build_and_start(); .build_and_start();
if let Ok(uuid) = crate::shared::read_auth_token() { authorize_or_redirect();
send_ws_msg(WsMsg::AuthorizeRequest(uuid));
};
let cell_app = std::sync::RwLock::new(app); let cell_app = std::sync::RwLock::new(app);
unsafe { unsafe {
APP = Some(cell_app); 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"]);
}
};
}
};
}

View File

@ -1,8 +1,65 @@
use crate::{model, Msg};
use seed::{prelude::*, *}; use seed::{prelude::*, *};
pub fn update(_msg: Msg, _model: &mut model::Model, _orders: &mut impl Orders<Msg>) {} 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<Msg> { pub fn update(msg: Msg, model: &mut model::Model, _orders: &mut impl Orders<Msg>) {
div![] if model.page != Page::Login {
return;
}
match msg {
_ => (),
};
}
pub fn view(model: &model::Model) -> Node<Msg> {
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)
} }

View File

@ -9,7 +9,7 @@ use crate::shared::styled_input::StyledInput;
use crate::shared::styled_modal::StyledModal; use crate::shared::styled_modal::StyledModal;
use crate::shared::tracking_widget::tracking_widget; use crate::shared::tracking_widget::tracking_widget;
use crate::shared::{find_issue, ToNode}; 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<Msg> { pub fn view(model: &Model, issue_id: IssueId) -> Node<Msg> {
let _issue = match find_issue(model, issue_id) { let _issue = match find_issue(model, issue_id) {

View File

@ -14,6 +14,10 @@ use crate::shared::{drag_ev, inner_layout, ToNode};
use crate::{EditIssueModalFieldId, FieldId, Msg}; use crate::{EditIssueModalFieldId, FieldId, Msg};
pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Orders<Msg>) { pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Orders<Msg>) {
if model.user.is_none() {
return;
}
match &model.page { match &model.page {
Page::Project | Page::AddIssue | Page::EditIssue(..) => { Page::Project | Page::AddIssue | Page::EditIssue(..) => {
model.page_content = PageContent::Project(ProjectPage::default()); model.page_content = PageContent::Project(ProjectPage::default());

View File

@ -16,6 +16,10 @@ use crate::FieldChange::TabChanged;
use crate::{model, FieldId, Msg, ProjectSettingsFieldId}; use crate::{model, FieldId, Msg, ProjectSettingsFieldId};
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.user.is_none() {
return;
}
if model.page != Page::ProjectSettings { if model.page != Page::ProjectSettings {
log!("not settings page"); log!("not settings page");
return; return;

View File

@ -51,22 +51,33 @@ pub fn inner_layout(
] ]
} }
pub fn outer_layout(_model: &Model, page_name: &str, children: Vec<Node<Msg>>) -> Node<Msg> {
article![
class!["outer-layout", "outerPage"],
id![page_name],
children
]
}
pub fn write_auth_token(token: Option<uuid::Uuid>) -> Result<Msg, String> { pub fn write_auth_token(token: Option<uuid::Uuid>) -> Result<Msg, String> {
let w = window(); let w = window();
let store = match w.local_storage() { let store = match w.local_storage() {
Ok(Some(store)) => store, Ok(Some(store)) => store,
_ => return Err("Local storage is not available".to_string()), _ => return Err("Local storage is not available".to_string()),
}; };
store match token {
.set_item( Some(token) => {
"authToken", store
token .set_item("authToken", format!("{}", token).as_str())
.as_ref() .map_err(|_e| "Failed to read auth token".to_string())?;
.map(|t| format!("{}", t)) }
.unwrap_or_default() _ => {
.as_str(), store
) .remove_item("authToken")
.map_err(|_e| "Failed to read auth token".to_string())?; .map_err(|_e| "Failed to read auth token".to_string())?;
}
}
Ok(match token { Ok(match token {
Some(_) => Msg::AuthTokenStored, Some(_) => Msg::AuthTokenStored,
_ => Msg::AuthTokenErased, _ => Msg::AuthTokenErased,

View File

@ -170,10 +170,10 @@ impl StyledIconBuilder {
} }
pub struct StyledIcon { pub struct StyledIcon {
pub icon: Icon, icon: Icon,
pub size: Option<i32>, size: Option<i32>,
pub class_list: Vec<String>, class_list: Vec<String>,
pub style_list: Vec<String>, style_list: Vec<String>,
} }
impl StyledIcon { impl StyledIcon {

View File

@ -2,6 +2,7 @@ use seed::prelude::*;
use jirs_data::WsMsg; use jirs_data::WsMsg;
use crate::shared::write_auth_token;
use crate::{model, Msg, APP}; use crate::{model, Msg, APP};
pub mod issue; pub mod issue;
@ -24,6 +25,13 @@ pub fn update(msg: &Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>
Msg::WsMsg(WsMsg::AuthorizeLoaded(Ok(user))) => { Msg::WsMsg(WsMsg::AuthorizeLoaded(Ok(user))) => {
model.user = Some(user.clone()); 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 // project
Msg::WsMsg(WsMsg::ProjectLoaded(project)) => { Msg::WsMsg(WsMsg::ProjectLoaded(project)) => {
model.project = Some(project.clone()); model.project = Some(project.clone());