This commit is contained in:
2019-01-02 23:52:17 +03:00
parent 29a035912b
commit 3bebc24684
8 changed files with 250 additions and 13 deletions

View File

@@ -1,6 +1,7 @@
import * as React from "react";
import { Route, Switch } from "react-router";
import { Login } from "~Auth/Login";
import { Signup } from "~Auth/Signup";
import { Home } from "~Home";
export function App() {
@@ -9,6 +10,7 @@ export function App() {
<Switch>
<Route exact={true} path="/" component={Home} />
<Route path="/login" component={Login} />
<Route path="/signup" component={Signup} />
</Switch>
</>
);

View File

@@ -9,10 +9,16 @@
h2 {
margin-bottom: 1rem;
}
.buttons {
.header,
.footer {
display: flex;
flex-direction: row;
align-items: baseline;
button.change {
margin-left: auto;
}
}
.footer {
#error {
height: 1rem;
color: $pt-intent-danger;

View File

@@ -3,11 +3,12 @@ import "./Auth.scss";
import { Button, Card, FormGroup, H2, InputGroup } from "@blueprintjs/core";
import * as React from "react";
import { connect } from "react-redux";
import { RouteComponentProps, withRouter } from "react-router-dom";
import { Dispatch } from "redux";
import { authStart } from "~redux/auth/actions";
import { IAppState } from "~redux/reducers";
interface ILoginComponentProps {
interface ILoginComponentProps extends RouteComponentProps {
inProgress: boolean;
error: string;
spinner: boolean;
@@ -26,10 +27,15 @@ export class LoginComponent extends React.PureComponent<
constructor(props: ILoginComponentProps) {
super(props);
this.submit = this.submit.bind(this);
this.change = this.change.bind(this);
this.updateFields = this.updateFields.bind(this);
this.state = { username: "", password: "" };
}
public change() {
this.props.history.push("/signup");
}
public submit() {
const { username, password } = this.state;
if (!this.props.inProgress) {
@@ -47,7 +53,17 @@ export class LoginComponent extends React.PureComponent<
<>
<Card className="AuthForm" elevation={2}>
<form>
<H2>Login</H2>
<div className="header">
<H2>Login</H2>
<Button
icon="new-person"
minimal={true}
onClick={this.change}
className="change"
>
Signup
</Button>
</div>
<FormGroup label="Username">
<InputGroup
name="username"
@@ -65,12 +81,13 @@ export class LoginComponent extends React.PureComponent<
leftIcon="key"
/>
</FormGroup>
<div className="buttons">
<div className="footer">
<div id="error">{this.props.error}</div>
<Button
loading={this.props.spinner}
className="submit"
intent="primary"
icon="log-in"
onClick={this.submit}
disabled={this.props.spinner}
>
@@ -99,7 +116,9 @@ function mapDispatchToProps(dispatch: Dispatch) {
};
}
export const Login = connect(
mapStateToProps,
mapDispatchToProps,
)(LoginComponent);
export const Login = withRouter(
connect(
mapStateToProps,
mapDispatchToProps,
)(LoginComponent),
);

View File

@@ -0,0 +1,135 @@
import "./Auth.scss";
import { Button, Card, FormGroup, H2, InputGroup } from "@blueprintjs/core";
import * as React from "react";
import { connect } from "react-redux";
import { RouteComponentProps, withRouter } from "react-router";
import { Dispatch } from "redux";
import { signupStart } from "~redux/auth/actions";
import { IAppState } from "~redux/reducers";
interface ISignupComponentProps extends RouteComponentProps {
inProgress: boolean;
error: string;
spinner: boolean;
signup: (username: string, password: string, email: string) => void;
}
interface ISignupComponentState {
username: string;
password: string;
email: string;
}
export class SignupComponent extends React.PureComponent<
ISignupComponentProps,
ISignupComponentState
> {
constructor(props: ISignupComponentProps) {
super(props);
this.submit = this.submit.bind(this);
this.change = this.change.bind(this);
this.updateFields = this.updateFields.bind(this);
this.state = { username: "", password: "", email: "" };
}
public change() {
this.props.history.push("/login");
}
public submit() {
const { username, password, email } = this.state;
if (!this.props.inProgress) {
this.props.signup(username, password, email);
}
}
public updateFields(e: React.FormEvent<HTMLInputElement>) {
const { value, name } = e.currentTarget;
this.setState({ ...this.state, [name]: value });
}
public render() {
return (
<>
<Card className="AuthForm" elevation={2}>
<form>
<div className="header">
<H2>Signup</H2>
<Button
icon="log-in"
minimal={true}
onClick={this.change}
className="change"
>
Login
</Button>
</div>
<FormGroup label="Username">
<InputGroup
name="username"
value={this.state.username}
onChange={this.updateFields}
leftIcon="person"
/>
</FormGroup>
<FormGroup label="Email">
<InputGroup
name="email"
value={this.state.email}
onChange={this.updateFields}
type="email"
leftIcon="envelope"
/>
</FormGroup>
<FormGroup label="Password">
<InputGroup
name="password"
value={this.state.password}
onChange={this.updateFields}
type="password"
leftIcon="key"
/>
</FormGroup>
<div className="footer">
<div id="error">{this.props.error}</div>
<Button
loading={this.props.spinner}
icon="new-person"
className="submit"
intent="primary"
onClick={this.submit}
disabled={this.props.spinner}
>
Signup
</Button>
</div>
</form>
</Card>
</>
);
}
}
function mapStateToProps(state: IAppState) {
return {
inProgress: state.auth.inProgress,
error: state.auth.formError,
spinner: state.auth.formSpinner,
};
}
function mapDispatchToProps(dispatch: Dispatch) {
return {
signup: (username: string, password: string, email: string) =>
dispatch(signupStart(username, password, email)),
};
}
export const Signup = withRouter(
connect(
mapStateToProps,
mapDispatchToProps,
)(SignupComponent),
);

View File

@@ -6,3 +6,15 @@ export async function login(username: string, password: string) {
password,
});
}
export async function signup(
username: string,
password: string,
email: string,
) {
return fetchJSON("/users/signup", "POST", {
username,
password,
email,
});
}

View File

@@ -2,6 +2,7 @@ import { Action } from "redux";
export enum AuthTypes {
AUTH_START = "AUTH_START",
SIGNUP_START = "SIGNUP_START",
AUTH_SUCCESS = "AUTH_SUCCESS",
AUTH_FAIL = "AUTH_FAIL",
AUTH_START_FORM_SPINNER = "AUTH_START_FORM_SPINNER",
@@ -15,6 +16,15 @@ export interface IAuthStartActionAction extends Action {
};
}
export interface ISignupStartActionAction extends Action {
type: AuthTypes.SIGNUP_START;
payload: {
username: string;
password: string;
email: string;
};
}
export interface IAuthSuccessActionAction extends Action {
type: AuthTypes.AUTH_SUCCESS;
payload: {
@@ -44,6 +54,17 @@ export function authStart(
return { type: AuthTypes.AUTH_START, payload: { username, password } };
}
export function signupStart(
username: string,
password: string,
email: string,
): ISignupStartActionAction {
return {
type: AuthTypes.SIGNUP_START,
payload: { username, password, email },
};
}
export function authSuccess(jwt: string): IAuthSuccessActionAction {
return { type: AuthTypes.AUTH_SUCCESS, payload: { jwt } };
}
@@ -56,4 +77,5 @@ export type AuthAction =
| IAuthStartActionAction
| IAuthSuccessActionAction
| IAuthFailureActionAction
| IAuthStartFormSpinnerAction;
| IAuthStartFormSpinnerAction
| ISignupStartActionAction;

View File

@@ -22,6 +22,7 @@ export const auth: Reducer<IAuthState, AuthAction> = (
) => {
switch (action.type) {
case AuthTypes.AUTH_START:
case AuthTypes.SIGNUP_START:
return { ...defaultAuthState, inProgress: true };
break;
case AuthTypes.AUTH_SUCCESS:

View File

@@ -1,6 +1,14 @@
import { delay } from "redux-saga";
import { call, cancel, fork, put, race, takeLatest } from "redux-saga/effects";
import { login } from "~redux/api/auth";
import {
all,
call,
cancel,
fork,
put,
race,
takeLatest,
} from "redux-saga/effects";
import { login, signup } from "~redux/api/auth";
import { setToken } from "~redux/api/utils";
import {
@@ -8,6 +16,7 @@ import {
authSuccess,
AuthTypes,
IAuthStartActionAction,
ISignupStartActionAction,
startFormSpinner,
} from "./actions";
@@ -44,6 +53,37 @@ function* authStart(action: IAuthStartActionAction) {
}
}
export function* authSaga() {
yield takeLatest(AuthTypes.AUTH_START, authStart);
function* signupStart(action: ISignupStartActionAction) {
const { username, password, email } = action.payload;
try {
const spinner = yield fork(startSpinner);
const { response, timeout } = yield race({
response: call(signup, username, password, email),
timeout: call(delay, 10000),
});
yield cancel(spinner);
if (timeout) {
yield put(authFail("Timeout"));
return;
}
if (response.data) {
const user = response.data;
yield call(setToken, user.jwt);
yield put(authSuccess(user.jwt));
} else {
yield put(authFail(response.error));
}
} catch (e) {
yield put(authFail(e.toString()));
}
}
export function* authSaga() {
yield all([
takeLatest(AuthTypes.AUTH_START, authStart),
takeLatest(AuthTypes.SIGNUP_START, signupStart),
]);
}