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 {
|
: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);
|
||||||
}
|
}
|
||||||
|
@ -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";
|
||||||
|
@ -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"]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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());
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
||||||
|
@ -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 {
|
||||||
|
@ -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());
|
||||||
|
Loading…
Reference in New Issue
Block a user