diff --git a/src/components/flow/TestGrid/index.tsx b/src/components/flow/TestGrid/index.tsx index 82cfd2cb..be1ad186 100644 --- a/src/components/flow/TestGrid/index.tsx +++ b/src/components/flow/TestGrid/index.tsx @@ -1,13 +1,7 @@ import * as React from 'react'; import classnames from 'classnames'; -// import * as AutoResponsive from 'autoresponsive-react'; -// const ReactGridLayout = require('react-grid-layout'); -// import 'react-grid-layout/css/styles.css'; -// import 'react-resizable/css/styles.css'; const style = require('./style.scss'); -// const Packery = require('react-packery-component')(React); -// http://37.192.131.144/hero/photos/photo-20120825-1532512.jpg export const TestGrid = () => (
@@ -23,50 +17,3 @@ export const TestGrid = () => (
); - -// export const TestGrid = () => ( -// -//
-//
-//
-//
-//
-//
-//
-// -// ); - -// export const TestGrid = () => ( -// -//
-//
-//
-//
-//
-//
-//
-//
-//
-//
-//
-//
-//
-//
-//
-// -// ); diff --git a/src/components/flow/TestGrid/style.scss b/src/components/flow/TestGrid/style.scss index 87bb58af..e3e229aa 100644 --- a/src/components/flow/TestGrid/style.scss +++ b/src/components/flow/TestGrid/style.scss @@ -14,8 +14,8 @@ $cols: $content_width / $cell; grid-auto-rows: 256px; grid-auto-flow: row dense; - grid-column-gap: 4px; - grid-row-gap: 4px; + grid-column-gap: $grid_line; + grid-row-gap: $grid_line; } .cell { @@ -25,6 +25,7 @@ $cols: $content_width / $cell; flex: 0 0; background: $cell_bg; + @include outer_shadow(); //&::after { // content: ' '; // background: transparentize(white, 0.9); diff --git a/src/components/input/Button/index.tsx b/src/components/input/Button/index.tsx new file mode 100644 index 00000000..d820805b --- /dev/null +++ b/src/components/input/Button/index.tsx @@ -0,0 +1,20 @@ +import * as React from 'react'; + +const style = require('./style.scss'); + +interface IButtonProps { + children?: string, + label?: string, + + onClick?: React.MouseEventHandler, +} + +export const Button: React.FunctionComponent = ({ + children, + label, + onClick = () => {}, +}) => ( +
+ {label || children || ''} +
+); diff --git a/src/components/input/Button/style.scss b/src/components/input/Button/style.scss new file mode 100644 index 00000000..63c59b7b --- /dev/null +++ b/src/components/input/Button/style.scss @@ -0,0 +1,14 @@ +.container { + height: $input_height; + border-radius: $input_radius; + display: flex; + background: $button_bg_color; + align-items: center; + justify-content: center; + text-transform: uppercase; + font-weight: 600; + font-size: $text_small; + cursor: pointer; + + @include outer_shadow(); +} diff --git a/src/components/input/Info/index.tsx b/src/components/input/Info/index.tsx new file mode 100644 index 00000000..c4a6accb --- /dev/null +++ b/src/components/input/Info/index.tsx @@ -0,0 +1,21 @@ +import * as React from 'react'; +import classNames = require("classnames"); + +const style = require('./style.scss'); + +interface IInfoProps { + text?: string, + children?: string, + level?: string, +} +export const Info: React.FunctionComponent = ({ + text, + children, + level = 'normal', +}) => ( +
+ { + text || children || '' + } +
+); diff --git a/src/components/input/Info/style.scss b/src/components/input/Info/style.scss new file mode 100644 index 00000000..55419617 --- /dev/null +++ b/src/components/input/Info/style.scss @@ -0,0 +1,28 @@ +.container { + min-height: $info_height; + border-radius: $input_radius; + display: flex; + align-items: center; + justify-content: center; + font-size: $text_small; + line-height: 1.2em; + padding: $gap; + background: transparentize(white, 0.9); + + &:global(.danger) { + color: white; + background: transparentize($color_red, 0.5); + } + + &:global(.warning) { + color: white; + background: transparentize($color_yellow, 0.5); + } + + &:global(.primary) { + color: white; + background: transparentize($color_blue, 0.5); + } + +} + diff --git a/src/components/input/TextInput/index.tsx b/src/components/input/TextInput/index.tsx new file mode 100644 index 00000000..eaf22641 --- /dev/null +++ b/src/components/input/TextInput/index.tsx @@ -0,0 +1,38 @@ +import * as React from 'react'; + +const style = require('./style.scss'); + +interface ITextInputProps { + type?: 'text' | 'password', + placeholder?: string, + label?: string, + value?: string, + + onChange: React.ChangeEventHandler, +} + +export const TextInput: React.FunctionComponent = ({ + type = 'text', + placeholder = '', + label, + onChange = () => {}, + value='', +}) => ( +
+ { + label && +
{label}
+ } +
+ +
+
+); diff --git a/src/components/input/TextInput/style.scss b/src/components/input/TextInput/style.scss new file mode 100644 index 00000000..291556ee --- /dev/null +++ b/src/components/input/TextInput/style.scss @@ -0,0 +1,37 @@ +.wrapper { + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.label { + background: $input_bg_color; + font-size: 10px; + text-transform: uppercase; + font-weight: 600; + padding: 2px $gap; +} + +.container { + height: $input_height; + background: $input_bg_color; + border-radius: $input_radius; + flex-direction: row; + flex: 1 0; + display: flex; + align-self: stretch; + align-items: center; + justify-content: center; + @include input_shadow(); +} + +.input { + outline: none; + background: transparent; + flex: 1; + border: none; + font-size: inherit; + color: white; + padding: 0 $gap; + box-sizing: border-box; +} diff --git a/src/components/login/LoginForm/index.tsx b/src/components/login/LoginForm/index.tsx new file mode 100644 index 00000000..e4af94a6 --- /dev/null +++ b/src/components/login/LoginForm/index.tsx @@ -0,0 +1,103 @@ +import * as React from 'react'; +import { TextInput } from "$components/input/TextInput"; +import { Button } from "$components/input/Button"; +import { connect } from 'react-redux'; +import { bindActionCreators } from "redux"; +import { userSendLoginRequest, userSetLoginError } from "$redux/user/actions"; +import { IUserFormStateLogin, IUserState } from "$redux/user/reducer"; +import { Info } from "$components/input/Info"; + +const login = require('$containers/LoginLayout/style'); +const style = require('./style.scss'); + +interface ILoginFormProps { + error: IUserFormStateLogin['error'], + + userSendLoginRequest: typeof userSendLoginRequest, + userSetLoginError: typeof userSetLoginError, +} + +interface ILoginFormState { + username: string, + password: string, +} + +class Component extends React.PureComponent { + state = { + username: 'user', + password: 'password', + }; + + sendRequest = () => { + console.log('send?'); + this.props.userSendLoginRequest(this.state); + }; + + changeField = (field: T) => ({ target: { value }}: React.ChangeEvent) => { + if (this.props.error) this.props.userSetLoginError({ error: null }); + this.setState({ [field]: value } as Pick); + }; + + render() { + const { error } = this.props; + const { username, password } = this.state; + + return ( +
+
+
+ +
+
+
+ РЕШИТЕЛЬНО
ВОЙТИ +
+ +
+ +
+ + +
+ + + +
+ + + + { + error && + +
+ + {error} + + + } +
+
+
+
+ ) + } +} + +const mapStateToProps = ({ user: { form_state: { login }}}: { user: IUserState }) => ({ ...login }); +const mapDispatchToProps = dispatch => bindActionCreators({ + userSendLoginRequest, + userSetLoginError, +}, dispatch); + +export const LoginForm = connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/src/components/login/LoginForm/style.scss b/src/components/login/LoginForm/style.scss new file mode 100644 index 00000000..adc0baf6 --- /dev/null +++ b/src/components/login/LoginForm/style.scss @@ -0,0 +1,43 @@ +.container { + display: grid; + flex: 1; + + grid-template-columns: repeat(4, 1fr); + grid-template-rows: 1fr; + grid-row-gap: $grid_line; + grid-column-gap: $grid_line; +} + +.area_left { + grid-column-end: span 3; + background: $content_bg_color; + padding: $spc; + border-radius: $panel_radius 0 0 $panel_radius; + display: flex; + align-items: center; + justify-content: center; + + @include outer_shadow(); +} + +.area_right { + grid-column-end: span 1; + background: $content_bg_secondary; + padding: $spc; + border-radius: $panel_radius 0 0 $panel_radius; + user-select: none; + + @include outer_shadow(); +} + +.area_sign { + font-size: $text_sign; + font-weight: 800; + line-height: 1.2em; +} + +.inputs { + display: flex; + align-items: stretch; + flex-direction: column; +} diff --git a/src/components/main/Header/index.tsx b/src/components/main/Header/index.tsx index fde983df..0616868a 100644 --- a/src/components/main/Header/index.tsx +++ b/src/components/main/Header/index.tsx @@ -1,12 +1,12 @@ import * as React from 'react'; +import { Logo } from "$components/main/Logo"; + const style = require('./style.scss'); export const Header = () => (
-
- VAULT -
+
depth
diff --git a/src/components/main/Header/style.scss b/src/components/main/Header/style.scss index 19cb7471..e97570fa 100644 --- a/src/components/main/Header/style.scss +++ b/src/components/main/Header/style.scss @@ -7,11 +7,6 @@ height: 100px; } -.logo { - font-size: 1.4em; - font-weight: 800; - display: flex; -} .spacer { flex: 1; @@ -36,7 +31,12 @@ display: block; } - &:last-child::after { display: none; } + &:last-child { + padding-right: 0; + + &::after { display: none; } + } + } } diff --git a/src/components/main/Logo/index.tsx b/src/components/main/Logo/index.tsx new file mode 100644 index 00000000..f1397106 --- /dev/null +++ b/src/components/main/Logo/index.tsx @@ -0,0 +1,8 @@ +import * as React from 'react'; +const style = require('./style.scss'); + +export const Logo = () => ( +
+ VAULT +
+); diff --git a/src/components/main/Logo/style.scss b/src/components/main/Logo/style.scss new file mode 100644 index 00000000..1f45b651 --- /dev/null +++ b/src/components/main/Logo/style.scss @@ -0,0 +1,5 @@ +.logo { + font-size: $text_sign; + font-weight: 800; + display: flex; +} diff --git a/src/constants/api.ts b/src/constants/api.ts new file mode 100644 index 00000000..b0629e42 --- /dev/null +++ b/src/constants/api.ts @@ -0,0 +1,6 @@ +export const API = { + BASE: 'http://localhost:3000', + USER: { + LOGIN: '/user/login', + } +}; diff --git a/src/containers/App.tsx b/src/containers/App.tsx index 625c7c13..22fbf38b 100644 --- a/src/containers/App.tsx +++ b/src/containers/App.tsx @@ -2,12 +2,11 @@ import * as React from 'react'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import { hot } from 'react-hot-loader'; -import { SomeComponent } from '$components/SomeComponent'; import { ConnectedRouter } from "connected-react-router"; import { history } from "$redux/store"; import { NavLink, Switch, Route } from 'react-router-dom'; -import { MainLayout } from "$containers/MainLayout"; import { FlowLayout } from "$containers/FlowLayout"; +import { LoginLayout } from "$containers/LoginLayout"; interface IAppProps {} interface IAppState {} @@ -24,8 +23,8 @@ class Component extends React.Component { component={FlowLayout} />
diff --git a/src/containers/LoginLayout/index.tsx b/src/containers/LoginLayout/index.tsx new file mode 100644 index 00000000..c2dec9a8 --- /dev/null +++ b/src/containers/LoginLayout/index.tsx @@ -0,0 +1,13 @@ +import * as React from 'react'; +import { LoginForm } from '$components/login/LoginForm'; +import { MainLayout } from "$containers/MainLayout"; + +const style = require('./style.scss'); + +export const LoginLayout: React.FunctionComponent<{}> = () => ( + +
+ +
+
+); diff --git a/src/containers/LoginLayout/style.scss b/src/containers/LoginLayout/style.scss new file mode 100644 index 00000000..d7feb87f --- /dev/null +++ b/src/containers/LoginLayout/style.scss @@ -0,0 +1,15 @@ +.container { + +} + +.form { + width: $content_width; + min-height: $cell * 2; + box-sizing: border-box; + border-radius: $panel_radius; + display: flex; + align-items: stretch; + justify-content: stretch; + + //@include outer_shadow(); +} diff --git a/src/redux/user/actions.ts b/src/redux/user/actions.ts index 6f30ba12..0074719a 100644 --- a/src/redux/user/actions.ts +++ b/src/redux/user/actions.ts @@ -1,3 +1,16 @@ import { USER_ACTIONS } from "$redux/user/constants"; +import { IUserProfile } from "$redux/user/reducer"; -export const someAction = () => ({ type: USER_ACTIONS.SOME_ACTION }); +export const userSendLoginRequest = ({ + username, password +}: { + username: string, password: string +}) => ({ type: USER_ACTIONS.SEND_LOGIN_REQUEST, username, password }); + +export const userSetLoginError = ({ + error +}: { + error: string +}) => ({ type: USER_ACTIONS.SET_LOGIN_ERROR, error }); + +export const userSetUser = (profile: Partial) => ({ type: USER_ACTIONS.SET_USER, profile }); diff --git a/src/redux/user/api.ts b/src/redux/user/api.ts new file mode 100644 index 00000000..65e716dd --- /dev/null +++ b/src/redux/user/api.ts @@ -0,0 +1,12 @@ +import { api } from "$utils/api"; +import { API } from "$constants/api"; +import { IApiUser } from "$redux/user/constants"; + +export const apiUserLogin = ( + { username, password }: + { username: string, password: string } +): Promise<{ token: string, status?: number, user?: IApiUser }> => ( + api.post(API.USER.LOGIN, { username, password }) + .then(r => r && r.data && { token: r.data.token, user: r.data.user, status: 200 }) + .catch( (r) => ({ token: '', user: null, status: parseInt(r.response.status) })) +); diff --git a/src/redux/user/constants.ts b/src/redux/user/constants.ts index aca439d2..a5aa84d2 100644 --- a/src/redux/user/constants.ts +++ b/src/redux/user/constants.ts @@ -1,3 +1,24 @@ export const USER_ACTIONS = { - SOME_ACTION: 'SOME_ACTION', + SEND_LOGIN_REQUEST: 'SEND_LOGIN_REQUEST', + SET_LOGIN_ERROR: 'SET_LOGIN_ERROR', + SET_USER: 'SET_USER', }; + +export const USER_ERRORS = { + INVALID_CREDENTIALS: 'Неверное имя пользователя или пароль. Очень жаль.', + EMPTY_CREDENTIALS: 'Давайте введем логин и пароль. Это обязательно.' +}; + +export const USER_STATUSES = { + 404: USER_ERRORS.INVALID_CREDENTIALS, +}; + +export interface IApiUser { + id: number, + username: string, + email: string, + role: string, + activated: boolean, + createdAt: string, + updatedAt: string, +} diff --git a/src/redux/user/reducer.ts b/src/redux/user/reducer.ts index 165a2a7a..8711b319 100644 --- a/src/redux/user/reducer.ts +++ b/src/redux/user/reducer.ts @@ -1,5 +1,5 @@ import { createReducer } from 'reduxsauce'; -import * as ACTIONS from "$redux/user/actions"; +import * as ActionCreators from "$redux/user/actions"; import { USER_ACTIONS } from "$redux/user/constants"; export interface IUserProfile { @@ -8,13 +8,14 @@ export interface IUserProfile { email: string, role: string, activated: boolean, + token: string, } export interface IUserFormStateLogin { error: string, } -export type IRootState = Readonly<{ +export type IUserState = Readonly<{ profile: IUserProfile, form_state: { login: IUserFormStateLogin, @@ -23,23 +24,40 @@ export type IRootState = Readonly<{ type UnsafeReturnType = T extends (...args: any[]) => infer R ? R : any; interface ActionHandler { - (state: IRootState, payload: UnsafeReturnType): IRootState; + (state: IUserState, payload: UnsafeReturnType): IUserState; } -const someActionHandler: ActionHandler = (state) => { - return { ...state }; -}; +const setLoginErrorHandler: ActionHandler = (state, { error }) => ({ + ...state, + form_state: { + ...state.form_state, + login: { + ...state.form_state.login, + error, + } + } +}); + +const setUserHandler: ActionHandler = (state, { profile }) => ({ + ...state, + profile: { + ...state.profile, + ...profile, + } +}); const HANDLERS = { - [USER_ACTIONS.SOME_ACTION]: someActionHandler, + [USER_ACTIONS.SET_LOGIN_ERROR]: setLoginErrorHandler, + [USER_ACTIONS.SET_USER]: setUserHandler, }; -const INITIAL_STATE: IRootState = { +const INITIAL_STATE: IUserState = { profile: { id: 0, username: '', email: '', role: '', + token: '', activated: false, }, form_state: { diff --git a/src/redux/user/sagas.ts b/src/redux/user/sagas.ts index 659d13ef..cc932012 100644 --- a/src/redux/user/sagas.ts +++ b/src/redux/user/sagas.ts @@ -1,25 +1,34 @@ -import { call, put, takeEvery, takeLatest } from 'redux-saga/effects'; -import { delay } from 'redux-saga'; -import { USER_ACTIONS } from "$redux/user/constants"; +import { call, put, takeLatest } from 'redux-saga/effects'; +import { SagaIterator } from 'redux-saga'; +import { IApiUser, USER_ACTIONS, USER_ERRORS, USER_STATUSES } from "$redux/user/constants"; +import * as ActionCreators from '$redux/user/actions'; +import { apiUserLogin } from "$redux/user/api"; +import { userSetLoginError, userSetUser } from "$redux/user/actions"; +import { push } from 'connected-react-router' -// Worker Saga for SET_EDITOR_LOCATION_INPUT reducer -/* -function* fetchSuggestions({ payload }) { - const { value } = payload; +function* sendLoginRequestSaga({ username, password }: ReturnType): SagaIterator { + if (!username || !password) return yield put(userSetLoginError({ error: USER_ERRORS.EMPTY_CREDENTIALS })); - yield delay(300); - try { - const results = yield call(someFunction, arguments); - yield put({ type: TYPES.ANOTHER_ACTION, payload: { results } }); - } catch (e) { - yield put({ type: TYPES.ANOTHER_ACTION, payload: { results } }); - } + const { token, status, user }: { token: string, status: number, user: IApiUser } = yield call(apiUserLogin, { username, password }); + + if (!token) return yield put(userSetLoginError({ error: USER_STATUSES[status] || USER_ERRORS.INVALID_CREDENTIALS })); + + const { id, role, email, activated } = user; + + yield put(userSetUser({ + token, + id, + role, + email, + username: user.username, + activated, + })); + + yield put(push('/')); } -*/ function* mySaga() { - // fetch autocompletion on location input - //yield takeLatest(TYPES.ACTION, function); + yield takeLatest(USER_ACTIONS.SEND_LOGIN_REQUEST, sendLoginRequestSaga); } export default mySaga; diff --git a/src/styles/colors.scss b/src/styles/colors.scss index b614e702..af62e1f4 100644 --- a/src/styles/colors.scss +++ b/src/styles/colors.scss @@ -1,6 +1,21 @@ +$color_red: #ff3344; +$color_yellow: #ffd60f; +$color_blue: complement($color_red); +//$color_yellow: complement($color_red); +//$color_yellow: yellow; + $main_bg_color: #161616; $main_text_color: white; $content_bg_color: #222222; +$content_bg_secondary: darken($content_bg_color, 3%); $cell_bg: transparentize(white, 0.95); + +$text_normal: 16px; +$text_small: 14px; +$text_big: 20px; +$text_sign: 22px; + +$input_bg_color: transparentize(black, 0.8); +$button_bg_color: #ff3344; diff --git a/src/styles/global.scss b/src/styles/global.scss index 41554b46..45427fd0 100644 --- a/src/styles/global.scss +++ b/src/styles/global.scss @@ -25,6 +25,18 @@ body { } } +:global(.gap) { + height: $gap; +} + +:global(.spc) { + height: $spc; + + &:global(.double) { height: $spc * 2; } + &:global(.quadro) { height: $spc * 4; } + &:global(.sixty) { height: $spc * 6; } +} + :global(.padded) { padding: $gap; } @@ -46,3 +58,4 @@ body { :global(.footer) { height: 40px; } + diff --git a/src/styles/variables.scss b/src/styles/variables.scss index 53e1dd3f..18de67fb 100644 --- a/src/styles/variables.scss +++ b/src/styles/variables.scss @@ -6,6 +6,33 @@ $gap: 8px; $spc: $gap * 2; $panel_radius: 1px; +$grid_line: 4px; + +$input_height: 32px; +$input_radius: 2px; + +$info_height: 24px; + @mixin outer_shadow() { - box-shadow: transparentize(white, 0.92) -1px -1px, transparentize(black, 0.8) 1px 1px; + box-shadow: inset transparentize(white, 0.95) 0 1px, + inset transparentize(black, 0.5) 0 -1px; +} + +@mixin inner_shadow() { + box-shadow: inset transparentize(white, 0.95) 0 -1px, + inset transparentize(black, 0.5) 0 1px; +} + +@mixin input_shadow() { + box-shadow: inset transparentize(white, 0.92) 0 -1px, + inset transparentize(black, 0.8) 0 1px; +} + +@mixin modal_mixin() { + position: fixed; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; } diff --git a/src/utils/api/index.ts b/src/utils/api/index.ts new file mode 100644 index 00000000..4cf829df --- /dev/null +++ b/src/utils/api/index.ts @@ -0,0 +1,6 @@ +import axios from 'axios'; +import { API } from "$constants/api"; + +export const api = axios.create({ + baseURL: API.BASE, +});