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 {
--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);
}

View File

@ -23,3 +23,4 @@
@import "./css/project.css";
@import "./css/projectSettings.css";
@import "./css/timeTracking.css";
@import "./css/login.css";

View File

@ -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<Msg>) {
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"]);
}
};
}
};
}

View File

@ -1,8 +1,65 @@
use crate::{model, Msg};
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> {
div![]
pub fn update(msg: Msg, model: &mut model::Model, _orders: &mut impl Orders<Msg>) {
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::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<Msg> {
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};
pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Orders<Msg>) {
if model.user.is_none() {
return;
}
match &model.page {
Page::Project | Page::AddIssue | Page::EditIssue(..) => {
model.page_content = PageContent::Project(ProjectPage::default());

View File

@ -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<Msg>) {
if model.user.is_none() {
return;
}
if model.page != Page::ProjectSettings {
log!("not settings page");
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> {
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,

View File

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

View File

@ -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>
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());