Finish time tracking, login page, validate token
This commit is contained in:
parent
350337f0ed
commit
5c3e8c8e93
38
jirs-client/js/css/login.css
Normal file
38
jirs-client/js/css/login.css
Normal 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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -23,3 +23,4 @@
|
||||
@import "./css/project.css";
|
||||
@import "./css/projectSettings.css";
|
||||
@import "./css/timeTracking.css";
|
||||
@import "./css/login.css";
|
||||
|
@ -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"]);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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());
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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());
|
||||
|
Loading…
Reference in New Issue
Block a user