mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-24 20:36:40 +07:00
login mechanism
This commit is contained in:
parent
6168841f78
commit
9528e7f699
27 changed files with 528 additions and 96 deletions
|
@ -1,13 +1,7 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import classnames from 'classnames';
|
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 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 = () => (
|
export const TestGrid = () => (
|
||||||
<div className={style.grid_test}>
|
<div className={style.grid_test}>
|
||||||
|
@ -23,50 +17,3 @@ export const TestGrid = () => (
|
||||||
<div className={classnames([style.cell, style.vert_1, style.hor_1])} key="j" />
|
<div className={classnames([style.cell, style.vert_1, style.hor_1])} key="j" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
// export const TestGrid = () => (
|
|
||||||
// <ReactGridLayout
|
|
||||||
// className="layout"
|
|
||||||
// cols={4}
|
|
||||||
// rowHeight={256}
|
|
||||||
// width={1024 + 256}
|
|
||||||
// layout={layout}
|
|
||||||
// margin={[0, 0]}
|
|
||||||
// compactType="vertical"
|
|
||||||
// verticalCompact
|
|
||||||
// >
|
|
||||||
// <div className={style.cell} key="a" />
|
|
||||||
// <div className={style.cell} key="b" />
|
|
||||||
// <div className={style.cell} key="c" />
|
|
||||||
// <div className={style.cell} key="d" />
|
|
||||||
// <div className={style.cell} key="e" />
|
|
||||||
// <div className={style.cell} key="f" />
|
|
||||||
// <div className={style.cell} key="g" />
|
|
||||||
// </ReactGridLayout>
|
|
||||||
// );
|
|
||||||
|
|
||||||
// export const TestGrid = () => (
|
|
||||||
// <AutoResponsive
|
|
||||||
// itemMargin={0}
|
|
||||||
// containerWidth={1024 + 256}
|
|
||||||
// itemClassName={style.cell}
|
|
||||||
// gridWidth={256}
|
|
||||||
// transitionDuration={0}
|
|
||||||
// >
|
|
||||||
// <div style={{ width: 256 * 4, height: 256 * 2 }} className={style.cell} key="a" />
|
|
||||||
// <div style={{ width: 256, height: 256 * 2 }} className={style.cell} key="b" />
|
|
||||||
// <div style={{ width: 256, height: 256 }} className={style.cell} key="c" />
|
|
||||||
// <div style={{ width: 256, height: 256 }} className={style.cell} key="d" />
|
|
||||||
// <div style={{ width: 256, height: 256 }} className={style.cell} key="d" />
|
|
||||||
// <div style={{ width: 256 * 2, height: 256 * 2 }} className={style.cell} key="h" />
|
|
||||||
// <div style={{ width: 256 * 2, height: 256 }} className={style.cell} key="e" />
|
|
||||||
// <div style={{ width: 256 * 2, height: 256 }} className={style.cell} key="f" />
|
|
||||||
// <div style={{ width: 256 * 2, height: 256 }} className={style.cell} key="g" />
|
|
||||||
// <div style={{ width: 256 * 2, height: 256 }} className={style.cell} key="g1" />
|
|
||||||
// <div style={{ width: 256, height: 256 }} className={style.cell} key="d" />
|
|
||||||
// <div style={{ width: 256, height: 256 }} className={style.cell} key="d1" />
|
|
||||||
// <div style={{ width: 256, height: 256 }} className={style.cell} key="d2" />
|
|
||||||
// <div style={{ width: 256, height: 256 }} className={style.cell} key="d3" />
|
|
||||||
// <div style={{ width: 256, height: 256 }} className={style.cell} key="d4" />
|
|
||||||
// </AutoResponsive>
|
|
||||||
// );
|
|
||||||
|
|
|
@ -14,8 +14,8 @@ $cols: $content_width / $cell;
|
||||||
grid-auto-rows: 256px;
|
grid-auto-rows: 256px;
|
||||||
grid-auto-flow: row dense;
|
grid-auto-flow: row dense;
|
||||||
|
|
||||||
grid-column-gap: 4px;
|
grid-column-gap: $grid_line;
|
||||||
grid-row-gap: 4px;
|
grid-row-gap: $grid_line;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cell {
|
.cell {
|
||||||
|
@ -25,6 +25,7 @@ $cols: $content_width / $cell;
|
||||||
flex: 0 0;
|
flex: 0 0;
|
||||||
background: $cell_bg;
|
background: $cell_bg;
|
||||||
|
|
||||||
|
@include outer_shadow();
|
||||||
//&::after {
|
//&::after {
|
||||||
// content: ' ';
|
// content: ' ';
|
||||||
// background: transparentize(white, 0.9);
|
// background: transparentize(white, 0.9);
|
||||||
|
|
20
src/components/input/Button/index.tsx
Normal file
20
src/components/input/Button/index.tsx
Normal file
|
@ -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<IButtonProps> = ({
|
||||||
|
children,
|
||||||
|
label,
|
||||||
|
onClick = () => {},
|
||||||
|
}) => (
|
||||||
|
<div className={style.container} onClick={onClick}>
|
||||||
|
{label || children || ''}
|
||||||
|
</div>
|
||||||
|
);
|
14
src/components/input/Button/style.scss
Normal file
14
src/components/input/Button/style.scss
Normal file
|
@ -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();
|
||||||
|
}
|
21
src/components/input/Info/index.tsx
Normal file
21
src/components/input/Info/index.tsx
Normal file
|
@ -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<IInfoProps> = ({
|
||||||
|
text,
|
||||||
|
children,
|
||||||
|
level = 'normal',
|
||||||
|
}) => (
|
||||||
|
<div className={classNames(style.container, { [level]: true })}>
|
||||||
|
{
|
||||||
|
text || children || ''
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
28
src/components/input/Info/style.scss
Normal file
28
src/components/input/Info/style.scss
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
38
src/components/input/TextInput/index.tsx
Normal file
38
src/components/input/TextInput/index.tsx
Normal file
|
@ -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<ITextInputProps> = ({
|
||||||
|
type = 'text',
|
||||||
|
placeholder = '',
|
||||||
|
label,
|
||||||
|
onChange = () => {},
|
||||||
|
value='',
|
||||||
|
}) => (
|
||||||
|
<div
|
||||||
|
className={style.wrapper}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
label &&
|
||||||
|
<div className={style.label}>{label}</div>
|
||||||
|
}
|
||||||
|
<div className={style.container}>
|
||||||
|
<input
|
||||||
|
placeholder={placeholder}
|
||||||
|
className={style.input}
|
||||||
|
type={type}
|
||||||
|
onChange={onChange}
|
||||||
|
value={value}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
37
src/components/input/TextInput/style.scss
Normal file
37
src/components/input/TextInput/style.scss
Normal file
|
@ -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;
|
||||||
|
}
|
103
src/components/login/LoginForm/index.tsx
Normal file
103
src/components/login/LoginForm/index.tsx
Normal file
|
@ -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<ILoginFormProps, ILoginFormState> {
|
||||||
|
state = {
|
||||||
|
username: 'user',
|
||||||
|
password: 'password',
|
||||||
|
};
|
||||||
|
|
||||||
|
sendRequest = () => {
|
||||||
|
console.log('send?');
|
||||||
|
this.props.userSendLoginRequest(this.state);
|
||||||
|
};
|
||||||
|
|
||||||
|
changeField = <T extends keyof ILoginFormState>(field: T) => ({ target: { value }}: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (this.props.error) this.props.userSetLoginError({ error: null });
|
||||||
|
this.setState({ [field]: value } as Pick<ILoginFormState, keyof ILoginFormState>);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { error } = this.props;
|
||||||
|
const { username, password } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={login.form}>
|
||||||
|
<div className={style.container}>
|
||||||
|
<div className={style.area_left}>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div className={style.area_right}>
|
||||||
|
<div className={style.area_sign}>
|
||||||
|
РЕШИТЕЛЬНО<br />ВОЙТИ
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="spc double" />
|
||||||
|
|
||||||
|
<div className={style.inputs}>
|
||||||
|
<TextInput
|
||||||
|
label="Логин"
|
||||||
|
value={username}
|
||||||
|
onChange={this.changeField('username')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="gap" />
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
label="Пароль"
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={this.changeField('password')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="spc double" />
|
||||||
|
|
||||||
|
<Button onClick={this.sendRequest}>
|
||||||
|
Войти
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{
|
||||||
|
error &&
|
||||||
|
<React.Fragment>
|
||||||
|
<div className="spc" />
|
||||||
|
<Info>
|
||||||
|
{error}
|
||||||
|
</Info>
|
||||||
|
</React.Fragment>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = ({ user: { form_state: { login }}}: { user: IUserState }) => ({ ...login });
|
||||||
|
const mapDispatchToProps = dispatch => bindActionCreators({
|
||||||
|
userSendLoginRequest,
|
||||||
|
userSetLoginError,
|
||||||
|
}, dispatch);
|
||||||
|
|
||||||
|
export const LoginForm = connect(mapStateToProps, mapDispatchToProps)(Component);
|
43
src/components/login/LoginForm/style.scss
Normal file
43
src/components/login/LoginForm/style.scss
Normal file
|
@ -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;
|
||||||
|
}
|
|
@ -1,12 +1,12 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import { Logo } from "$components/main/Logo";
|
||||||
|
|
||||||
const style = require('./style.scss');
|
const style = require('./style.scss');
|
||||||
|
|
||||||
export const Header = () => (
|
export const Header = () => (
|
||||||
<div className="default_container head_container">
|
<div className="default_container head_container">
|
||||||
<div className={style.container}>
|
<div className={style.container}>
|
||||||
<div className={style.logo}>
|
<Logo />
|
||||||
VAULT
|
|
||||||
</div>
|
|
||||||
<div className={style.spacer} />
|
<div className={style.spacer} />
|
||||||
<div className={style.plugs}>
|
<div className={style.plugs}>
|
||||||
<div>depth</div>
|
<div>depth</div>
|
||||||
|
|
|
@ -7,11 +7,6 @@
|
||||||
height: 100px;
|
height: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
|
||||||
font-size: 1.4em;
|
|
||||||
font-weight: 800;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spacer {
|
.spacer {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
@ -36,7 +31,12 @@
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:last-child::after { display: none; }
|
&:last-child {
|
||||||
|
padding-right: 0;
|
||||||
|
|
||||||
|
&::after { display: none; }
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
8
src/components/main/Logo/index.tsx
Normal file
8
src/components/main/Logo/index.tsx
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
const style = require('./style.scss');
|
||||||
|
|
||||||
|
export const Logo = () => (
|
||||||
|
<div className={style.logo}>
|
||||||
|
VAULT
|
||||||
|
</div>
|
||||||
|
);
|
5
src/components/main/Logo/style.scss
Normal file
5
src/components/main/Logo/style.scss
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
.logo {
|
||||||
|
font-size: $text_sign;
|
||||||
|
font-weight: 800;
|
||||||
|
display: flex;
|
||||||
|
}
|
6
src/constants/api.ts
Normal file
6
src/constants/api.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export const API = {
|
||||||
|
BASE: 'http://localhost:3000',
|
||||||
|
USER: {
|
||||||
|
LOGIN: '/user/login',
|
||||||
|
}
|
||||||
|
};
|
|
@ -2,12 +2,11 @@ import * as React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { hot } from 'react-hot-loader';
|
import { hot } from 'react-hot-loader';
|
||||||
import { SomeComponent } from '$components/SomeComponent';
|
|
||||||
import { ConnectedRouter } from "connected-react-router";
|
import { ConnectedRouter } from "connected-react-router";
|
||||||
import { history } from "$redux/store";
|
import { history } from "$redux/store";
|
||||||
import { NavLink, Switch, Route } from 'react-router-dom';
|
import { NavLink, Switch, Route } from 'react-router-dom';
|
||||||
import { MainLayout } from "$containers/MainLayout";
|
|
||||||
import { FlowLayout } from "$containers/FlowLayout";
|
import { FlowLayout } from "$containers/FlowLayout";
|
||||||
|
import { LoginLayout } from "$containers/LoginLayout";
|
||||||
|
|
||||||
interface IAppProps {}
|
interface IAppProps {}
|
||||||
interface IAppState {}
|
interface IAppState {}
|
||||||
|
@ -24,8 +23,8 @@ class Component extends React.Component<IAppProps, IAppState> {
|
||||||
component={FlowLayout}
|
component={FlowLayout}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/somepath"
|
path="/login"
|
||||||
component={SomeComponent}
|
component={LoginLayout}
|
||||||
/>
|
/>
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
|
|
13
src/containers/LoginLayout/index.tsx
Normal file
13
src/containers/LoginLayout/index.tsx
Normal file
|
@ -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<{}> = () => (
|
||||||
|
<MainLayout>
|
||||||
|
<div className="default_container">
|
||||||
|
<LoginForm />
|
||||||
|
</div>
|
||||||
|
</MainLayout>
|
||||||
|
);
|
15
src/containers/LoginLayout/style.scss
Normal file
15
src/containers/LoginLayout/style.scss
Normal file
|
@ -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();
|
||||||
|
}
|
|
@ -1,3 +1,16 @@
|
||||||
import { USER_ACTIONS } from "$redux/user/constants";
|
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<IUserProfile>) => ({ type: USER_ACTIONS.SET_USER, profile });
|
||||||
|
|
12
src/redux/user/api.ts
Normal file
12
src/redux/user/api.ts
Normal file
|
@ -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) }))
|
||||||
|
);
|
|
@ -1,3 +1,24 @@
|
||||||
export const USER_ACTIONS = {
|
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,
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { createReducer } from 'reduxsauce';
|
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";
|
import { USER_ACTIONS } from "$redux/user/constants";
|
||||||
|
|
||||||
export interface IUserProfile {
|
export interface IUserProfile {
|
||||||
|
@ -8,13 +8,14 @@ export interface IUserProfile {
|
||||||
email: string,
|
email: string,
|
||||||
role: string,
|
role: string,
|
||||||
activated: boolean,
|
activated: boolean,
|
||||||
|
token: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IUserFormStateLogin {
|
export interface IUserFormStateLogin {
|
||||||
error: string,
|
error: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IRootState = Readonly<{
|
export type IUserState = Readonly<{
|
||||||
profile: IUserProfile,
|
profile: IUserProfile,
|
||||||
form_state: {
|
form_state: {
|
||||||
login: IUserFormStateLogin,
|
login: IUserFormStateLogin,
|
||||||
|
@ -23,23 +24,40 @@ export type IRootState = Readonly<{
|
||||||
|
|
||||||
type UnsafeReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
|
type UnsafeReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
|
||||||
interface ActionHandler<T> {
|
interface ActionHandler<T> {
|
||||||
(state: IRootState, payload: UnsafeReturnType<T>): IRootState;
|
(state: IUserState, payload: UnsafeReturnType<T>): IUserState;
|
||||||
}
|
}
|
||||||
|
|
||||||
const someActionHandler: ActionHandler<typeof ACTIONS.someAction> = (state) => {
|
const setLoginErrorHandler: ActionHandler<typeof ActionCreators.userSetLoginError> = (state, { error }) => ({
|
||||||
return { ...state };
|
...state,
|
||||||
};
|
form_state: {
|
||||||
|
...state.form_state,
|
||||||
|
login: {
|
||||||
|
...state.form_state.login,
|
||||||
|
error,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const setUserHandler: ActionHandler<typeof ActionCreators.userSetUser> = (state, { profile }) => ({
|
||||||
|
...state,
|
||||||
|
profile: {
|
||||||
|
...state.profile,
|
||||||
|
...profile,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const HANDLERS = {
|
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: {
|
profile: {
|
||||||
id: 0,
|
id: 0,
|
||||||
username: '',
|
username: '',
|
||||||
email: '',
|
email: '',
|
||||||
role: '',
|
role: '',
|
||||||
|
token: '',
|
||||||
activated: false,
|
activated: false,
|
||||||
},
|
},
|
||||||
form_state: {
|
form_state: {
|
||||||
|
|
|
@ -1,25 +1,34 @@
|
||||||
import { call, put, takeEvery, takeLatest } from 'redux-saga/effects';
|
import { call, put, takeLatest } from 'redux-saga/effects';
|
||||||
import { delay } from 'redux-saga';
|
import { SagaIterator } from 'redux-saga';
|
||||||
import { USER_ACTIONS } from "$redux/user/constants";
|
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* sendLoginRequestSaga({ username, password }: ReturnType<typeof ActionCreators.userSendLoginRequest>): SagaIterator {
|
||||||
/*
|
if (!username || !password) return yield put(userSetLoginError({ error: USER_ERRORS.EMPTY_CREDENTIALS }));
|
||||||
function* fetchSuggestions({ payload }) {
|
|
||||||
const { value } = payload;
|
|
||||||
|
|
||||||
yield delay(300);
|
const { token, status, user }: { token: string, status: number, user: IApiUser } = yield call(apiUserLogin, { username, password });
|
||||||
try {
|
|
||||||
const results = yield call(someFunction, arguments);
|
if (!token) return yield put(userSetLoginError({ error: USER_STATUSES[status] || USER_ERRORS.INVALID_CREDENTIALS }));
|
||||||
yield put({ type: TYPES.ANOTHER_ACTION, payload: { results } });
|
|
||||||
} catch (e) {
|
const { id, role, email, activated } = user;
|
||||||
yield put({ type: TYPES.ANOTHER_ACTION, payload: { results } });
|
|
||||||
|
yield put(userSetUser({
|
||||||
|
token,
|
||||||
|
id,
|
||||||
|
role,
|
||||||
|
email,
|
||||||
|
username: user.username,
|
||||||
|
activated,
|
||||||
|
}));
|
||||||
|
|
||||||
|
yield put(push('/'));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
function* mySaga() {
|
function* mySaga() {
|
||||||
// fetch autocompletion on location input
|
yield takeLatest(USER_ACTIONS.SEND_LOGIN_REQUEST, sendLoginRequestSaga);
|
||||||
//yield takeLatest(TYPES.ACTION, function);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default mySaga;
|
export default mySaga;
|
||||||
|
|
|
@ -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_bg_color: #161616;
|
||||||
$main_text_color: white;
|
$main_text_color: white;
|
||||||
|
|
||||||
$content_bg_color: #222222;
|
$content_bg_color: #222222;
|
||||||
|
$content_bg_secondary: darken($content_bg_color, 3%);
|
||||||
|
|
||||||
$cell_bg: transparentize(white, 0.95);
|
$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;
|
||||||
|
|
|
@ -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) {
|
:global(.padded) {
|
||||||
padding: $gap;
|
padding: $gap;
|
||||||
}
|
}
|
||||||
|
@ -46,3 +58,4 @@ body {
|
||||||
:global(.footer) {
|
:global(.footer) {
|
||||||
height: 40px;
|
height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,33 @@ $gap: 8px;
|
||||||
$spc: $gap * 2;
|
$spc: $gap * 2;
|
||||||
$panel_radius: 1px;
|
$panel_radius: 1px;
|
||||||
|
|
||||||
|
$grid_line: 4px;
|
||||||
|
|
||||||
|
$input_height: 32px;
|
||||||
|
$input_radius: 2px;
|
||||||
|
|
||||||
|
$info_height: 24px;
|
||||||
|
|
||||||
@mixin outer_shadow() {
|
@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;
|
||||||
}
|
}
|
||||||
|
|
6
src/utils/api/index.ts
Normal file
6
src/utils/api/index.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
import { API } from "$constants/api";
|
||||||
|
|
||||||
|
export const api = axios.create({
|
||||||
|
baseURL: API.BASE,
|
||||||
|
});
|
Loading…
Add table
Add a link
Reference in a new issue