mirror of
https://github.com/usatiuk/photos.git
synced 2025-10-28 15:27:49 +01:00
transplant frontend auth from writer app
This commit is contained in:
4262
frontend/package-lock.json
generated
4262
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -12,8 +12,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@blueprintjs/core": "^3.33.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.4.0",
|
"@typescript-eslint/eslint-plugin": "^4.4.0",
|
||||||
"@typescript-eslint/parser": "^4.4.0",
|
"@typescript-eslint/parser": "^4.4.0",
|
||||||
|
"autoprefixer": "^9.8.6",
|
||||||
"enzyme": "^3.11.0",
|
"enzyme": "^3.11.0",
|
||||||
"enzyme-adapter-react-16": "^1.15.5",
|
"enzyme-adapter-react-16": "^1.15.5",
|
||||||
"eslint": "^7.10.0",
|
"eslint": "^7.10.0",
|
||||||
@@ -27,21 +29,36 @@
|
|||||||
"eslint-plugin-react": "^7.21.3",
|
"eslint-plugin-react": "^7.21.3",
|
||||||
"eslint-plugin-react-hooks": "^4.1.2",
|
"eslint-plugin-react-hooks": "^4.1.2",
|
||||||
"jest": "^26.5.2",
|
"jest": "^26.5.2",
|
||||||
"parcel": "^2.0.0-nightly.419",
|
"parcel": "^1.12.4",
|
||||||
|
"postcss": "^8.1.1",
|
||||||
"prettier": "^2.1.2",
|
"prettier": "^2.1.2",
|
||||||
"prettier-eslint": "^11.0.0",
|
"prettier-eslint": "^11.0.0",
|
||||||
"react": "^16.13.1",
|
"react": "^16.13.1",
|
||||||
"react-dom": "^16.13.1",
|
"react-dom": "^16.13.1",
|
||||||
|
"react-redux": "^7.2.1",
|
||||||
|
"react-router": "^5.2.0",
|
||||||
|
"react-router-dom": "^5.2.0",
|
||||||
|
"react-spring": "^8.0.27",
|
||||||
|
"redux": "^4.0.5",
|
||||||
|
"redux-devtools-extension": "^2.13.8",
|
||||||
|
"redux-persist": "^6.0.0",
|
||||||
|
"redux-saga": "^1.1.3",
|
||||||
|
"sass": "^1.27.0",
|
||||||
"ts-jest": "^26.4.1",
|
"ts-jest": "^26.4.1",
|
||||||
"typescript": "^4.0.3"
|
"typescript": "^4.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/autoprefixer": "^9.7.2",
|
||||||
"@types/enzyme": "^3.10.7",
|
"@types/enzyme": "^3.10.7",
|
||||||
"@types/enzyme-adapter-react-16": "^1.0.6",
|
"@types/enzyme-adapter-react-16": "^1.0.6",
|
||||||
"@types/eslint": "^7.2.4",
|
"@types/eslint": "^7.2.4",
|
||||||
"@types/eslint-plugin-prettier": "^3.1.0",
|
"@types/eslint-plugin-prettier": "^3.1.0",
|
||||||
"@types/prettier": "^2.1.1",
|
"@types/prettier": "^2.1.1",
|
||||||
"@types/react": "^16.9.51",
|
"@types/react": "^16.9.51",
|
||||||
"@types/react-dom": "^16.9.8"
|
"@types/react-dom": "^16.9.8",
|
||||||
|
"@types/react-redux": "^7.1.9",
|
||||||
|
"@types/react-router": "^5.1.8",
|
||||||
|
"@types/react-router-dom": "^5.1.6",
|
||||||
|
"@types/sass": "^1.16.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
0
frontend/src/Account/Account.scss
Normal file
0
frontend/src/Account/Account.scss
Normal file
76
frontend/src/Account/Account.tsx
Normal file
76
frontend/src/Account/Account.tsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { Button, Card, FormGroup, H2, InputGroup } from "@blueprintjs/core";
|
||||||
|
import * as React from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { Dispatch } from "redux";
|
||||||
|
import { IAppState } from "~redux/reducers";
|
||||||
|
import { userPassChange } from "~redux/user/actions";
|
||||||
|
|
||||||
|
export interface IAccountComponentProps {
|
||||||
|
username: string | undefined;
|
||||||
|
changePass: (password: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AccountComponent(props: IAccountComponentProps) {
|
||||||
|
const [pass, setPass] = React.useState("");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="AuthForm" elevation={2}>
|
||||||
|
<form
|
||||||
|
onSubmit={(e: React.FormEvent<any>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (pass.trim()) {
|
||||||
|
props.changePass(pass);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="header">
|
||||||
|
<H2>Account</H2>
|
||||||
|
</div>
|
||||||
|
<FormGroup label="Username">
|
||||||
|
<InputGroup
|
||||||
|
name="username"
|
||||||
|
leftIcon="person"
|
||||||
|
disabled={true}
|
||||||
|
value={props.username}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup label="Password">
|
||||||
|
<InputGroup
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
leftIcon="key"
|
||||||
|
value={pass}
|
||||||
|
onChange={(e: React.FormEvent<HTMLInputElement>) =>
|
||||||
|
setPass(e.currentTarget.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<div className="footer">
|
||||||
|
<Button
|
||||||
|
className="submit"
|
||||||
|
intent="primary"
|
||||||
|
icon="floppy-disk"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStateToProps(state: IAppState) {
|
||||||
|
return { username: state?.user?.user?.username };
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch: Dispatch) {
|
||||||
|
return {
|
||||||
|
changePass: (password: string) => dispatch(userPassChange(password)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Account = connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps,
|
||||||
|
)(AccountComponent);
|
||||||
17
frontend/src/Account/tests/Account.test.tsx
Normal file
17
frontend/src/Account/tests/Account.test.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { shallow } from "enzyme";
|
||||||
|
import { AccountComponent } from "../Account";
|
||||||
|
|
||||||
|
describe("<Account />", () => {
|
||||||
|
it("should not crash", () => {
|
||||||
|
const wrapper = shallow(
|
||||||
|
<AccountComponent
|
||||||
|
username="user"
|
||||||
|
changePass={(pass: string) => {
|
||||||
|
return null;
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
32
frontend/src/App.scss
Normal file
32
frontend/src/App.scss
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
@import "~@blueprintjs/core/lib/scss/variables";
|
||||||
|
|
||||||
|
.animationWrapper {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loadingWrapper {
|
||||||
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
|
height: 75%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bp3-navbar {
|
||||||
|
.bp3-button {
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: "PT Sans", "emoji", sans-serif;
|
||||||
|
|
||||||
|
background: $light-gray5;
|
||||||
|
}
|
||||||
42
frontend/src/App.tsx
Normal file
42
frontend/src/App.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import {
|
||||||
|
Redirect,
|
||||||
|
Route,
|
||||||
|
RouteComponentProps,
|
||||||
|
Switch,
|
||||||
|
withRouter,
|
||||||
|
} from "react-router";
|
||||||
|
import { AuthScreen } from "~Auth/AuthScreen";
|
||||||
|
import { Home } from "~Home/Home";
|
||||||
|
import { Landing } from "~Landing/Landing";
|
||||||
|
import { IAppState } from "~redux/reducers";
|
||||||
|
|
||||||
|
interface IAppComponentProps extends RouteComponentProps {
|
||||||
|
loggedIn: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AppComponent(props: IAppComponentProps) {
|
||||||
|
const { loggedIn } = props;
|
||||||
|
return loggedIn ? (
|
||||||
|
<Switch>
|
||||||
|
<Route path="/signup" component={AuthScreen} />,
|
||||||
|
<Route path="/login" component={AuthScreen} />,
|
||||||
|
<Route path="/docs/:id" component={Home} />,
|
||||||
|
<Route path="/" component={Home} />,
|
||||||
|
</Switch>
|
||||||
|
) : (
|
||||||
|
<Switch>
|
||||||
|
<Route path="/signup" component={AuthScreen} />
|
||||||
|
<Route path="/login" component={AuthScreen} />
|
||||||
|
<Route exact={true} path="/" component={Landing} />
|
||||||
|
<Route path="/" component={() => <Redirect to="/login" />} />
|
||||||
|
</Switch>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStateToProps(state: IAppState) {
|
||||||
|
return { loggedIn: state.auth.jwt !== null };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const App = withRouter(connect(mapStateToProps)(AppComponent));
|
||||||
22
frontend/src/AppToaster.tsx
Normal file
22
frontend/src/AppToaster.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Position, Toaster } from "@blueprintjs/core";
|
||||||
|
|
||||||
|
export const AppToaster = Toaster.create({
|
||||||
|
className: "recipe-toaster",
|
||||||
|
position: Position.TOP,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function showPasswordSavedToast() {
|
||||||
|
AppToaster.show({
|
||||||
|
message: "Password saved!",
|
||||||
|
intent: "success",
|
||||||
|
timeout: 2000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showPasswordNotSavedToast(error: string) {
|
||||||
|
AppToaster.show({
|
||||||
|
message: "Password not saved! " + error,
|
||||||
|
intent: "danger",
|
||||||
|
timeout: 2000,
|
||||||
|
});
|
||||||
|
}
|
||||||
41
frontend/src/Auth/Auth.scss
Normal file
41
frontend/src/Auth/Auth.scss
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
@import "~@blueprintjs/core/lib/scss/variables";
|
||||||
|
|
||||||
|
.AuthForm {
|
||||||
|
margin: auto;
|
||||||
|
margin-top: 10rem;
|
||||||
|
width: 20rem;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header,
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: baseline;
|
||||||
|
|
||||||
|
button.change {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
#error {
|
||||||
|
height: 1rem;
|
||||||
|
color: $pt-intent-danger;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.submit {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
69
frontend/src/Auth/AuthScreen.tsx
Normal file
69
frontend/src/Auth/AuthScreen.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import {
|
||||||
|
Redirect,
|
||||||
|
Route,
|
||||||
|
RouteComponentProps,
|
||||||
|
Switch,
|
||||||
|
withRouter,
|
||||||
|
} from "react-router";
|
||||||
|
import { animated, Transition } from "react-spring/renderprops";
|
||||||
|
import { IAppState } from "~redux/reducers";
|
||||||
|
|
||||||
|
import { Login } from "./Login";
|
||||||
|
import { Signup } from "./Signup";
|
||||||
|
|
||||||
|
interface IAuthScreenProps extends RouteComponentProps {
|
||||||
|
loggedIn: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AuthScreenComponent extends React.PureComponent<IAuthScreenProps> {
|
||||||
|
constructor(props: IAuthScreenProps) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
public render() {
|
||||||
|
const { location } = this.props.history;
|
||||||
|
const { from } = this.props.location.state || { from: "/" };
|
||||||
|
const { loggedIn } = this.props;
|
||||||
|
return loggedIn ? (
|
||||||
|
<Redirect to={from} />
|
||||||
|
) : (
|
||||||
|
<div className="animationWrapper">
|
||||||
|
<Transition
|
||||||
|
native={true}
|
||||||
|
items={location}
|
||||||
|
keys={location.pathname}
|
||||||
|
from={{
|
||||||
|
opacity: 0,
|
||||||
|
transform: "translate3d(-400px,0,0)",
|
||||||
|
}}
|
||||||
|
enter={{
|
||||||
|
opacity: 1,
|
||||||
|
transform: "translate3d(0,0,0)",
|
||||||
|
}}
|
||||||
|
leave={{
|
||||||
|
opacity: 0,
|
||||||
|
transform: "translate3d(400px,0,0)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(_location: any) => (style: any) => (
|
||||||
|
<animated.div style={style}>
|
||||||
|
<Switch location={_location}>
|
||||||
|
<Route path="/login" component={Login} />
|
||||||
|
<Route path="/signup" component={Signup} />
|
||||||
|
</Switch>
|
||||||
|
</animated.div>
|
||||||
|
)}
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStateToProps(state: IAppState) {
|
||||||
|
return { loggedIn: !(state.auth.jwt === null) };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AuthScreen = withRouter(
|
||||||
|
connect(mapStateToProps)(AuthScreenComponent),
|
||||||
|
);
|
||||||
123
frontend/src/Auth/Login.tsx
Normal file
123
frontend/src/Auth/Login.tsx
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
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 extends RouteComponentProps {
|
||||||
|
inProgress: boolean;
|
||||||
|
error: string;
|
||||||
|
spinner: boolean;
|
||||||
|
login: (username: string, password: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ILoginComponentState {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LoginComponent extends React.PureComponent<
|
||||||
|
ILoginComponentProps,
|
||||||
|
ILoginComponentState
|
||||||
|
> {
|
||||||
|
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(e: React.FormEvent<any>) {
|
||||||
|
e.preventDefault();
|
||||||
|
const { username, password } = this.state;
|
||||||
|
if (!this.props.inProgress) {
|
||||||
|
this.props.login(username, password);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 onSubmit={this.submit}>
|
||||||
|
<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"
|
||||||
|
value={this.state.username}
|
||||||
|
onChange={this.updateFields}
|
||||||
|
leftIcon="person"
|
||||||
|
/>
|
||||||
|
</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}
|
||||||
|
className="submit"
|
||||||
|
intent="primary"
|
||||||
|
icon="log-in"
|
||||||
|
type="submit"
|
||||||
|
onClick={this.submit}
|
||||||
|
disabled={this.props.spinner}
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</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 {
|
||||||
|
login: (username: string, password: string) =>
|
||||||
|
dispatch(authStart(username, password)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Login = withRouter(
|
||||||
|
connect(mapStateToProps, mapDispatchToProps)(LoginComponent),
|
||||||
|
);
|
||||||
134
frontend/src/Auth/Signup.tsx
Normal file
134
frontend/src/Auth/Signup.tsx
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
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(e: React.FormEvent<any>) {
|
||||||
|
e.preventDefault();
|
||||||
|
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 onSubmit={this.submit}>
|
||||||
|
<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"
|
||||||
|
type="submit"
|
||||||
|
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),
|
||||||
|
);
|
||||||
53
frontend/src/Home/Home.scss
Normal file
53
frontend/src/Home/Home.scss
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
@import "~@blueprintjs/core/lib/scss/variables";
|
||||||
|
|
||||||
|
.viewComponent {
|
||||||
|
position: absolute;
|
||||||
|
max-width: 100%;
|
||||||
|
width: 50rem;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: -$pt-navbar-height;
|
||||||
|
bottom: 0;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
padding-top: 2 * $pt-navbar-height + 20px;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mainContainer {
|
||||||
|
transition: 0.3s;
|
||||||
|
|
||||||
|
#MainScreen {
|
||||||
|
transition: 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bp3-navbar {
|
||||||
|
transition: 0.3s;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
// keeps the breadcrumbs from taking all the space
|
||||||
|
max-width: 65%;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
transition: 0.3s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#uploadingStatusButton {
|
||||||
|
width: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#mainContainer.bp3-dark {
|
||||||
|
transition: 0.3s;
|
||||||
|
|
||||||
|
#MainScreen {
|
||||||
|
transition: 0.3s;
|
||||||
|
background: $dark-gray3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bp3-navbar {
|
||||||
|
transition: 0.3s;
|
||||||
|
}
|
||||||
|
}
|
||||||
161
frontend/src/Home/Home.tsx
Normal file
161
frontend/src/Home/Home.tsx
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import "./Home.scss";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Alignment,
|
||||||
|
Breadcrumbs,
|
||||||
|
Button,
|
||||||
|
Classes,
|
||||||
|
IBreadcrumbProps,
|
||||||
|
Icon,
|
||||||
|
Menu,
|
||||||
|
Navbar,
|
||||||
|
Popover,
|
||||||
|
Spinner,
|
||||||
|
} from "@blueprintjs/core";
|
||||||
|
import * as React from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { Route, RouteComponentProps, Switch, withRouter } from "react-router";
|
||||||
|
import { animated, config, Transition } from "react-spring/renderprops";
|
||||||
|
import { Dispatch } from "redux";
|
||||||
|
import { IUserJSON } from "~../../src/entity/User";
|
||||||
|
import { Account } from "~Account/Account";
|
||||||
|
import { Overview } from "~Photos/Overview";
|
||||||
|
import { toggleDarkMode } from "~redux/localSettings/actions";
|
||||||
|
import { IAppState } from "~redux/reducers";
|
||||||
|
import { logoutUser } from "~redux/user/actions";
|
||||||
|
|
||||||
|
export interface IHomeProps extends RouteComponentProps {
|
||||||
|
user: IUserJSON | null;
|
||||||
|
|
||||||
|
darkMode: boolean;
|
||||||
|
|
||||||
|
logout: () => void;
|
||||||
|
dispatchToggleDarkMode: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HomeComponent extends React.PureComponent<IHomeProps> {
|
||||||
|
constructor(props: IHomeProps) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const { location } = this.props.history;
|
||||||
|
return (
|
||||||
|
this.props.user && (
|
||||||
|
<div
|
||||||
|
id="mainContainer"
|
||||||
|
className={this.props.darkMode ? Classes.DARK : undefined}
|
||||||
|
>
|
||||||
|
<Navbar fixedToTop={true}>
|
||||||
|
<Navbar.Group align={Alignment.LEFT}>
|
||||||
|
<Button
|
||||||
|
minimal={true}
|
||||||
|
onClick={() => this.props.history.push("/")}
|
||||||
|
>
|
||||||
|
Writer
|
||||||
|
</Button>
|
||||||
|
<Navbar.Divider />
|
||||||
|
</Navbar.Group>
|
||||||
|
<Navbar.Group align={Alignment.RIGHT}>
|
||||||
|
<Popover
|
||||||
|
target={
|
||||||
|
<Button id="userButton">
|
||||||
|
{this.props.user.username}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
content={this.menu()}
|
||||||
|
/>
|
||||||
|
</Navbar.Group>
|
||||||
|
</Navbar>
|
||||||
|
<div id="MainScreen" className="animationWrapper">
|
||||||
|
<Transition
|
||||||
|
native={true}
|
||||||
|
config={{
|
||||||
|
...config.default,
|
||||||
|
clamp: true,
|
||||||
|
precision: 0.1,
|
||||||
|
}}
|
||||||
|
items={location}
|
||||||
|
keys={location.pathname}
|
||||||
|
from={{
|
||||||
|
opacity: 0,
|
||||||
|
transform: "translate3d(-400px,0,0)",
|
||||||
|
}}
|
||||||
|
enter={{
|
||||||
|
opacity: 1,
|
||||||
|
transform: "translate3d(0,0,0)",
|
||||||
|
}}
|
||||||
|
leave={{
|
||||||
|
opacity: 0,
|
||||||
|
transform: "translate3d(400px,0,0)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(_location: any) => (style: any) => (
|
||||||
|
<animated.div
|
||||||
|
style={style}
|
||||||
|
className="viewComponent"
|
||||||
|
>
|
||||||
|
<Switch location={_location}>
|
||||||
|
<Route
|
||||||
|
path="/account"
|
||||||
|
component={Account}
|
||||||
|
/>
|
||||||
|
<Route path="/" component={Overview} />
|
||||||
|
</Switch>
|
||||||
|
</animated.div>
|
||||||
|
)}
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private menu() {
|
||||||
|
return (
|
||||||
|
<Menu>
|
||||||
|
<Menu.Item
|
||||||
|
icon="user"
|
||||||
|
text="Account"
|
||||||
|
onClick={() => this.props.history.push("/account")}
|
||||||
|
/>
|
||||||
|
<Menu.Item
|
||||||
|
icon="log-out"
|
||||||
|
text="Logout"
|
||||||
|
onClick={this.props.logout}
|
||||||
|
/>
|
||||||
|
{this.props.darkMode ? (
|
||||||
|
<Menu.Item
|
||||||
|
icon="flash"
|
||||||
|
text="Light Mode"
|
||||||
|
onClick={this.props.dispatchToggleDarkMode}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Menu.Item
|
||||||
|
icon="moon"
|
||||||
|
text="Dark Mode"
|
||||||
|
onClick={this.props.dispatchToggleDarkMode}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStateToProps(state: IAppState) {
|
||||||
|
return {
|
||||||
|
user: state.user.user,
|
||||||
|
darkMode: state.localSettings.darkMode,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch: Dispatch) {
|
||||||
|
return {
|
||||||
|
logout: () => dispatch(logoutUser()),
|
||||||
|
dispatchToggleDarkMode: () => dispatch(toggleDarkMode()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Home = withRouter(
|
||||||
|
connect(mapStateToProps, mapDispatchToProps)(HomeComponent),
|
||||||
|
);
|
||||||
27
frontend/src/Home/tests/Home.test.tsx
Normal file
27
frontend/src/Home/tests/Home.test.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { shallow } from "enzyme";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { HomeComponent, IHomeProps } from "../Home";
|
||||||
|
|
||||||
|
const defaultHomeProps: IHomeProps = {
|
||||||
|
user: { id: 1, username: "test" },
|
||||||
|
|
||||||
|
darkMode: false,
|
||||||
|
|
||||||
|
logout: jest.fn(),
|
||||||
|
dispatchToggleDarkMode: jest.fn(),
|
||||||
|
|
||||||
|
history: { location: { pathname: "/" } } as any,
|
||||||
|
location: { pathname: "/" } as any,
|
||||||
|
match: {
|
||||||
|
params: {
|
||||||
|
id: null,
|
||||||
|
},
|
||||||
|
} as any,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("<Home />", () => {
|
||||||
|
it("should not crash", () => {
|
||||||
|
const wrapper = shallow(<HomeComponent {...defaultHomeProps} />);
|
||||||
|
});
|
||||||
|
});
|
||||||
26
frontend/src/Landing/Landing.tsx
Normal file
26
frontend/src/Landing/Landing.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { Alignment, Button, Navbar } from "@blueprintjs/core";
|
||||||
|
import * as React from "react";
|
||||||
|
import { RouteComponentProps, withRouter } from "react-router";
|
||||||
|
|
||||||
|
export function LandingComponent(props: RouteComponentProps) {
|
||||||
|
function login() {
|
||||||
|
props.history.push("/login");
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Navbar>
|
||||||
|
<Navbar.Group align={Alignment.LEFT}>
|
||||||
|
<Navbar.Heading>Writer</Navbar.Heading>
|
||||||
|
<Navbar.Divider />
|
||||||
|
</Navbar.Group>
|
||||||
|
<Navbar.Group align={Alignment.RIGHT}>
|
||||||
|
<Button icon="log-in" minimal={true} onClick={login}>
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
</Navbar.Group>
|
||||||
|
</Navbar>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Landing = withRouter(LandingComponent);
|
||||||
10
frontend/src/LoadingStub.tsx
Normal file
10
frontend/src/LoadingStub.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Spinner } from "@blueprintjs/core";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export function LoadingStub() {
|
||||||
|
return (
|
||||||
|
<div className="loadingWrapper">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
5
frontend/src/NotFound.tsx
Normal file
5
frontend/src/NotFound.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export function NotFound() {
|
||||||
|
return <div>404</div>;
|
||||||
|
}
|
||||||
29
frontend/src/Photos/Overview.tsx
Normal file
29
frontend/src/Photos/Overview.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import "./Photos.scss";
|
||||||
|
import * as React from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { Dispatch } from "redux";
|
||||||
|
import { IAppState } from "~redux/reducers";
|
||||||
|
|
||||||
|
export interface IOverviewComponentProps {
|
||||||
|
fetching: boolean;
|
||||||
|
spinner: boolean;
|
||||||
|
|
||||||
|
fetchDocs: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function OverviewComponent() {
|
||||||
|
return <div id="overview">Overview!</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStateToProps(state: IAppState) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch: Dispatch) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Overview = connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps,
|
||||||
|
)(OverviewComponent);
|
||||||
10
frontend/src/Photos/Photos.scss
Normal file
10
frontend/src/Photos/Photos.scss
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
@import "~@blueprintjs/core/lib/scss/variables";
|
||||||
|
|
||||||
|
#overview {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bp3-dark {
|
||||||
|
#overview {}
|
||||||
|
}
|
||||||
15
frontend/src/Photos/tests/Overview.test.tsx
Normal file
15
frontend/src/Photos/tests/Overview.test.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { shallow } from "enzyme";
|
||||||
|
import { OverviewComponent } from "../Overview";
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("<Overview />", () => {
|
||||||
|
it("should not crash", () => {
|
||||||
|
const wrapper = shallow(<OverviewComponent />);
|
||||||
|
expect(wrapper.contains("Overview!")).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
|
|
||||||
import { shallow } from "enzyme";
|
|
||||||
import { TestTest } from "~TestTest";
|
|
||||||
|
|
||||||
describe("<TestTest />", () => {
|
|
||||||
it("should not crash", () => {
|
|
||||||
const wrapper = shallow(<TestTest />);
|
|
||||||
expect(wrapper.contains(<div>Hello!</div>)).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
|
|
||||||
export function TestTest() {
|
|
||||||
return <div>Hello!</div>;
|
|
||||||
}
|
|
||||||
2
frontend/src/env.ts
Normal file
2
frontend/src/env.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export const apiRoot = process.env.API_ROOT || "http://localhost:3000";
|
||||||
|
export const webRoot = process.env.WEB_ROOT || "http://localhost:1234";
|
||||||
1
frontend/src/fileMock.ts
Normal file
1
frontend/src/fileMock.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
module.exports = "test-file-stub";
|
||||||
@@ -1,14 +1,20 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||||
|
|
||||||
<head>
|
<link
|
||||||
<meta charset="utf-8" />
|
href="https://fonts.googleapis.com/css?family=PT+Sans:400,400i,700,700i&display=swap&subset=cyrillic,cyrillic-ext,latin-ext"
|
||||||
<title>My Parcel Project</title>
|
rel="stylesheet"
|
||||||
</head>
|
/>
|
||||||
|
|
||||||
<body>
|
<title>Writer</title>
|
||||||
<div id="root"></div>
|
</head>
|
||||||
<script src="./index.tsx"></script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
<body>
|
||||||
|
<div id="body"></div>
|
||||||
|
<script src="./index.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|||||||
@@ -1,4 +1,23 @@
|
|||||||
|
import "@blueprintjs/core/lib/css/blueprint.css";
|
||||||
|
import "@blueprintjs/icons/lib/css/blueprint-icons.css";
|
||||||
|
import "normalize.css/normalize.css";
|
||||||
|
import "~App.scss";
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { render } from "react-dom";
|
import { render } from "react-dom";
|
||||||
|
import { Provider } from "react-redux";
|
||||||
|
import { BrowserRouter } from "react-router-dom";
|
||||||
|
import { PersistGate } from "redux-persist/integration/react";
|
||||||
|
import { App } from "~App";
|
||||||
|
import { persistor, store } from "~redux/store";
|
||||||
|
|
||||||
render(<h1>Hello World</h1>, document.getElementById("root"));
|
render(
|
||||||
|
<Provider store={store}>
|
||||||
|
<PersistGate loading={null} persistor={persistor}>
|
||||||
|
<BrowserRouter>
|
||||||
|
<App />
|
||||||
|
</BrowserRouter>
|
||||||
|
</PersistGate>
|
||||||
|
</Provider>,
|
||||||
|
document.getElementById("body"),
|
||||||
|
);
|
||||||
|
|||||||
25
frontend/src/redux/api/auth/index.ts
Normal file
25
frontend/src/redux/api/auth/index.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { IUserAuthJSON } from "~../../src/entity/User";
|
||||||
|
import { IAPIResponse } from "~../../src/types";
|
||||||
|
import { fetchJSON } from "../utils";
|
||||||
|
|
||||||
|
export async function login(
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
): Promise<IAPIResponse<IUserAuthJSON>> {
|
||||||
|
return (fetchJSON("/users/login", "POST", {
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
}) as unknown) as Promise<IAPIResponse<IUserAuthJSON>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function signup(
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
email: string,
|
||||||
|
): Promise<IAPIResponse<IUserAuthJSON>> {
|
||||||
|
return (fetchJSON("/users/signup", "POST", {
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
email,
|
||||||
|
}) as unknown) as Promise<IAPIResponse<IUserAuthJSON>>;
|
||||||
|
}
|
||||||
15
frontend/src/redux/api/user/index.ts
Normal file
15
frontend/src/redux/api/user/index.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { fetchJSON, fetchJSONAuth } from "../utils";
|
||||||
|
import { IAPIResponse } from "~/../../src/types";
|
||||||
|
import { IUserAuthJSON, IUserJSON } from "~../../src/entity/User";
|
||||||
|
|
||||||
|
export async function fetchUser(): Promise<IAPIResponse<IUserJSON>> {
|
||||||
|
return (fetchJSONAuth("/users/user", "GET") as unknown) as Promise<
|
||||||
|
IAPIResponse<IUserAuthJSON>
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function changeUserPassword(newPassword: string) {
|
||||||
|
return (fetchJSONAuth("/users/edit", "POST", {
|
||||||
|
password: newPassword,
|
||||||
|
}) as unknown) as Promise<IAPIResponse<IUserAuthJSON>>;
|
||||||
|
}
|
||||||
52
frontend/src/redux/api/utils.ts
Normal file
52
frontend/src/redux/api/utils.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { apiRoot } from "~env";
|
||||||
|
|
||||||
|
let token: string | null;
|
||||||
|
|
||||||
|
export function setToken(_token: string): void {
|
||||||
|
token = _token;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getToken(): string | null {
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteToken(): void {
|
||||||
|
token = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchJSON(
|
||||||
|
path: string,
|
||||||
|
method: string,
|
||||||
|
body?: string | Record<string, unknown>,
|
||||||
|
headers?: Record<string, string>,
|
||||||
|
): Promise<Record<string, unknown>> {
|
||||||
|
if (typeof body === "object") {
|
||||||
|
body = JSON.stringify(body);
|
||||||
|
}
|
||||||
|
const response = await fetch(apiRoot + path, {
|
||||||
|
method,
|
||||||
|
body,
|
||||||
|
headers: {
|
||||||
|
...headers,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const json = (await response.json()) as Record<string, unknown>;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchJSONAuth(
|
||||||
|
path: string,
|
||||||
|
method: string,
|
||||||
|
body?: string | Record<string, unknown>,
|
||||||
|
headers?: Record<string, unknown>,
|
||||||
|
): Promise<Record<string, unknown>> {
|
||||||
|
if (token) {
|
||||||
|
return fetchJSON(path, method, body, {
|
||||||
|
...headers,
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error("Not logged in");
|
||||||
|
}
|
||||||
|
}
|
||||||
80
frontend/src/redux/auth/actions.ts
Normal file
80
frontend/src/redux/auth/actions.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { Action } from "redux";
|
||||||
|
import { IUserAuthJSON } from "~../../src/entity/User";
|
||||||
|
|
||||||
|
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",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAuthStartAction extends Action {
|
||||||
|
type: AuthTypes.AUTH_START;
|
||||||
|
payload: {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISignupStartAction extends Action {
|
||||||
|
type: AuthTypes.SIGNUP_START;
|
||||||
|
payload: {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAuthSuccessAction extends Action {
|
||||||
|
type: AuthTypes.AUTH_SUCCESS;
|
||||||
|
payload: IUserAuthJSON;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAuthFailureAction extends Action {
|
||||||
|
type: AuthTypes.AUTH_FAIL;
|
||||||
|
payload: {
|
||||||
|
error: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAuthStartFormSpinnerAction extends Action {
|
||||||
|
type: AuthTypes.AUTH_START_FORM_SPINNER;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startFormSpinner(): IAuthStartFormSpinnerAction {
|
||||||
|
return { type: AuthTypes.AUTH_START_FORM_SPINNER };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function authStart(
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
): IAuthStartAction {
|
||||||
|
return { type: AuthTypes.AUTH_START, payload: { username, password } };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function signupStart(
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
email: string,
|
||||||
|
): ISignupStartAction {
|
||||||
|
return {
|
||||||
|
type: AuthTypes.SIGNUP_START,
|
||||||
|
payload: { username, password, email },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function authSuccess(user: IUserAuthJSON): IAuthSuccessAction {
|
||||||
|
return { type: AuthTypes.AUTH_SUCCESS, payload: user };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function authFail(error: string): IAuthFailureAction {
|
||||||
|
return { type: AuthTypes.AUTH_FAIL, payload: { error } };
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AuthAction =
|
||||||
|
| IAuthStartAction
|
||||||
|
| IAuthSuccessAction
|
||||||
|
| IAuthFailureAction
|
||||||
|
| IAuthStartFormSpinnerAction
|
||||||
|
| ISignupStartAction;
|
||||||
56
frontend/src/redux/auth/reducer.ts
Normal file
56
frontend/src/redux/auth/reducer.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { Reducer } from "react";
|
||||||
|
|
||||||
|
import { setToken } from "~redux/api/utils";
|
||||||
|
import { UserAction, UserTypes } from "~redux/user/actions";
|
||||||
|
import { AuthAction, AuthTypes } from "./actions";
|
||||||
|
|
||||||
|
export interface IAuthState {
|
||||||
|
jwt: string | null;
|
||||||
|
inProgress: boolean;
|
||||||
|
formError: string | null;
|
||||||
|
formSpinner: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultAuthState: IAuthState = {
|
||||||
|
jwt: null,
|
||||||
|
inProgress: false,
|
||||||
|
formError: null,
|
||||||
|
formSpinner: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const authReducer: Reducer<IAuthState, AuthAction> = (
|
||||||
|
state: IAuthState = defaultAuthState,
|
||||||
|
action: AuthAction | UserAction,
|
||||||
|
): IAuthState => {
|
||||||
|
switch (action.type) {
|
||||||
|
case AuthTypes.AUTH_START:
|
||||||
|
case AuthTypes.SIGNUP_START:
|
||||||
|
return { ...defaultAuthState, inProgress: true };
|
||||||
|
break;
|
||||||
|
case AuthTypes.AUTH_SUCCESS:
|
||||||
|
case UserTypes.USER_GET_SUCCESS:
|
||||||
|
setToken(action.payload.jwt);
|
||||||
|
return {
|
||||||
|
...defaultAuthState,
|
||||||
|
jwt: action.payload.jwt,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case UserTypes.USER_GET_FAIL:
|
||||||
|
if (action.payload.logout) {
|
||||||
|
return defaultAuthState;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case AuthTypes.AUTH_FAIL:
|
||||||
|
return { ...defaultAuthState, formError: action.payload.error };
|
||||||
|
break;
|
||||||
|
case AuthTypes.AUTH_START_FORM_SPINNER:
|
||||||
|
return { ...state, formSpinner: true };
|
||||||
|
case UserTypes.USER_LOGOUT:
|
||||||
|
return defaultAuthState;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
};
|
||||||
86
frontend/src/redux/auth/sagas.ts
Normal file
86
frontend/src/redux/auth/sagas.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import {
|
||||||
|
all,
|
||||||
|
call,
|
||||||
|
cancel,
|
||||||
|
delay,
|
||||||
|
fork,
|
||||||
|
put,
|
||||||
|
race,
|
||||||
|
takeLatest,
|
||||||
|
} from "redux-saga/effects";
|
||||||
|
import { login, signup } from "~redux/api/auth";
|
||||||
|
|
||||||
|
import {
|
||||||
|
authFail,
|
||||||
|
authSuccess,
|
||||||
|
AuthTypes,
|
||||||
|
IAuthStartAction,
|
||||||
|
ISignupStartAction,
|
||||||
|
startFormSpinner,
|
||||||
|
} from "./actions";
|
||||||
|
|
||||||
|
function* startSpinner() {
|
||||||
|
yield delay(300);
|
||||||
|
yield put(startFormSpinner());
|
||||||
|
}
|
||||||
|
|
||||||
|
function* authStart(action: IAuthStartAction) {
|
||||||
|
const { username, password } = action.payload;
|
||||||
|
try {
|
||||||
|
const spinner = yield fork(startSpinner);
|
||||||
|
|
||||||
|
const { response, timeout } = yield race({
|
||||||
|
response: call(login, username, password),
|
||||||
|
timeout: delay(10000),
|
||||||
|
});
|
||||||
|
|
||||||
|
yield cancel(spinner);
|
||||||
|
|
||||||
|
if (timeout) {
|
||||||
|
yield put(authFail("Timeout"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (response.data) {
|
||||||
|
const user = response.data;
|
||||||
|
yield put(authSuccess(user));
|
||||||
|
} else {
|
||||||
|
yield put(authFail(response.error));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
yield put(authFail("Internal error"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function* signupStart(action: ISignupStartAction) {
|
||||||
|
const { username, password, email } = action.payload;
|
||||||
|
try {
|
||||||
|
const spinner = yield fork(startSpinner);
|
||||||
|
|
||||||
|
const { response, timeout } = yield race({
|
||||||
|
response: call(signup, username, password, email),
|
||||||
|
timeout: delay(10000),
|
||||||
|
});
|
||||||
|
|
||||||
|
yield cancel(spinner);
|
||||||
|
|
||||||
|
if (timeout) {
|
||||||
|
yield put(authFail("Timeout"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (response.data) {
|
||||||
|
const user = response.data;
|
||||||
|
yield put(authSuccess(user));
|
||||||
|
} 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),
|
||||||
|
]);
|
||||||
|
}
|
||||||
15
frontend/src/redux/localSettings/actions.ts
Normal file
15
frontend/src/redux/localSettings/actions.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { Action } from "redux";
|
||||||
|
|
||||||
|
export enum LocalSettingsTypes {
|
||||||
|
TOGGLE_DARK_MODE = "TOGGLE_DARK_MODE",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IToggleDarkModeAction extends Action {
|
||||||
|
type: LocalSettingsTypes.TOGGLE_DARK_MODE;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toggleDarkMode(): IToggleDarkModeAction {
|
||||||
|
return { type: LocalSettingsTypes.TOGGLE_DARK_MODE };
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LocalSettingsAction = IToggleDarkModeAction;
|
||||||
32
frontend/src/redux/localSettings/reducer.ts
Normal file
32
frontend/src/redux/localSettings/reducer.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { Reducer } from "react";
|
||||||
|
import { UserAction, UserTypes } from "~redux/user/actions";
|
||||||
|
|
||||||
|
import { LocalSettingsAction, LocalSettingsTypes } from "./actions";
|
||||||
|
|
||||||
|
export interface ILocalSettingsState {
|
||||||
|
darkMode: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultLocalSettingsState: ILocalSettingsState = {
|
||||||
|
darkMode: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const localSettingsReducer: Reducer<
|
||||||
|
ILocalSettingsState,
|
||||||
|
LocalSettingsAction
|
||||||
|
> = (
|
||||||
|
state: ILocalSettingsState = defaultLocalSettingsState,
|
||||||
|
action: LocalSettingsAction | UserAction,
|
||||||
|
): ILocalSettingsState => {
|
||||||
|
const { darkMode } = state;
|
||||||
|
switch (action.type) {
|
||||||
|
case LocalSettingsTypes.TOGGLE_DARK_MODE:
|
||||||
|
return { ...state, darkMode: !darkMode };
|
||||||
|
case UserTypes.USER_LOGOUT:
|
||||||
|
return defaultLocalSettingsState;
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
};
|
||||||
36
frontend/src/redux/reducers.ts
Normal file
36
frontend/src/redux/reducers.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { combineReducers } from "redux";
|
||||||
|
import { persistReducer } from "redux-persist";
|
||||||
|
import { PersistPartial } from "redux-persist/es/persistReducer";
|
||||||
|
import storage from "redux-persist/lib/storage";
|
||||||
|
import { authReducer, IAuthState } from "~redux/auth/reducer";
|
||||||
|
import {
|
||||||
|
ILocalSettingsState,
|
||||||
|
localSettingsReducer,
|
||||||
|
} from "./localSettings/reducer";
|
||||||
|
import { IUserState, userReducer } from "./user/reducer";
|
||||||
|
|
||||||
|
export interface IAppState {
|
||||||
|
auth: IAuthState & PersistPartial;
|
||||||
|
user: IUserState;
|
||||||
|
localSettings: ILocalSettingsState & PersistPartial;
|
||||||
|
}
|
||||||
|
|
||||||
|
const authPersistConfig = {
|
||||||
|
key: "auth",
|
||||||
|
storage,
|
||||||
|
whitelist: ["jwt"],
|
||||||
|
};
|
||||||
|
|
||||||
|
const localSettingsPersistConfig = {
|
||||||
|
key: "localSettings",
|
||||||
|
storage,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const rootReducer = combineReducers({
|
||||||
|
auth: persistReducer<IAuthState>(authPersistConfig, authReducer),
|
||||||
|
user: userReducer,
|
||||||
|
localSettings: persistReducer(
|
||||||
|
localSettingsPersistConfig,
|
||||||
|
localSettingsReducer,
|
||||||
|
),
|
||||||
|
});
|
||||||
28
frontend/src/redux/store.ts
Normal file
28
frontend/src/redux/store.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { applyMiddleware, createStore } from "redux";
|
||||||
|
import { composeWithDevTools } from "redux-devtools-extension";
|
||||||
|
import { persistStore } from "redux-persist";
|
||||||
|
import createSagaMiddlware from "redux-saga";
|
||||||
|
import { rootReducer } from "~redux/reducers";
|
||||||
|
|
||||||
|
import { setToken } from "./api/utils";
|
||||||
|
import { authSaga } from "./auth/sagas";
|
||||||
|
import { getUser } from "./user/actions";
|
||||||
|
import { userSaga } from "./user/sagas";
|
||||||
|
|
||||||
|
const sagaMiddleware = createSagaMiddlware();
|
||||||
|
|
||||||
|
export const store = createStore(
|
||||||
|
rootReducer,
|
||||||
|
composeWithDevTools(applyMiddleware(sagaMiddleware)),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const persistor = persistStore(store, null, () => {
|
||||||
|
const state = store.getState();
|
||||||
|
if (state.auth.jwt) {
|
||||||
|
setToken(state.auth.jwt);
|
||||||
|
store.dispatch(getUser());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sagaMiddleware.run(authSaga);
|
||||||
|
sagaMiddleware.run(userSaga);
|
||||||
0
frontend/src/redux/types.ts
Normal file
0
frontend/src/redux/types.ts
Normal file
102
frontend/src/redux/user/actions.ts
Normal file
102
frontend/src/redux/user/actions.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import { Action } from "redux";
|
||||||
|
import { IUserAuthJSON, IUserJSON } from "~../../src/entity/User";
|
||||||
|
import { showPasswordNotSavedToast, showPasswordSavedToast } from "~AppToaster";
|
||||||
|
|
||||||
|
export enum UserTypes {
|
||||||
|
USER_GET = "USER_GET",
|
||||||
|
USER_GET_SUCCESS = "USER_GET_SUCCESS",
|
||||||
|
USER_GET_FAIL = "USER_GET_FAIL",
|
||||||
|
USER_LOGOUT = "USER_LOGOUT",
|
||||||
|
USER_PASS_CHANGE = "USER_PASS_CHANGE",
|
||||||
|
USER_PASS_CHANGE_SUCCESS = "USER_PASS_CHANGE_SUCCESS",
|
||||||
|
USER_PASS_CHANGE_FAIL = "USER_PASS_CHANGE_FAIL",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IUserGetAction extends Action {
|
||||||
|
type: UserTypes.USER_GET;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IUserLogoutAction extends Action {
|
||||||
|
type: UserTypes.USER_LOGOUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IUserGetSuccessAction extends Action {
|
||||||
|
type: UserTypes.USER_GET_SUCCESS;
|
||||||
|
payload: IUserAuthJSON;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IUserGetFailAction extends Action {
|
||||||
|
type: UserTypes.USER_GET_FAIL;
|
||||||
|
payload: {
|
||||||
|
error: string;
|
||||||
|
logout: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IUserPassChangeAction extends Action {
|
||||||
|
type: UserTypes.USER_PASS_CHANGE;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IUserPassChangeSuccessAction extends Action {
|
||||||
|
type: UserTypes.USER_PASS_CHANGE_SUCCESS;
|
||||||
|
payload: IUserAuthJSON;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IUserPassChangeFailAction extends Action {
|
||||||
|
type: UserTypes.USER_PASS_CHANGE_FAIL;
|
||||||
|
payload: {
|
||||||
|
error: string;
|
||||||
|
logout: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUser(): IUserGetAction {
|
||||||
|
return { type: UserTypes.USER_GET };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logoutUser(): IUserLogoutAction {
|
||||||
|
return { type: UserTypes.USER_LOGOUT };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUserSuccess(user: IUserAuthJSON): IUserGetSuccessAction {
|
||||||
|
return { type: UserTypes.USER_GET_SUCCESS, payload: user };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUserFail(
|
||||||
|
error: string,
|
||||||
|
logout: boolean,
|
||||||
|
): IUserGetFailAction {
|
||||||
|
return { type: UserTypes.USER_GET_FAIL, payload: { error, logout } };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userPassChange(password: string): IUserPassChangeAction {
|
||||||
|
return { type: UserTypes.USER_PASS_CHANGE, password };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userPassChangeSuccess(
|
||||||
|
user: IUserAuthJSON,
|
||||||
|
): IUserPassChangeSuccessAction {
|
||||||
|
showPasswordSavedToast();
|
||||||
|
return { type: UserTypes.USER_PASS_CHANGE_SUCCESS, payload: user };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userPassChangeFail(
|
||||||
|
error: string,
|
||||||
|
logout: boolean,
|
||||||
|
): IUserPassChangeFailAction {
|
||||||
|
showPasswordNotSavedToast(error);
|
||||||
|
return {
|
||||||
|
type: UserTypes.USER_PASS_CHANGE_FAIL,
|
||||||
|
payload: { error, logout },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserAction =
|
||||||
|
| IUserGetAction
|
||||||
|
| IUserGetSuccessAction
|
||||||
|
| IUserGetFailAction
|
||||||
|
| IUserLogoutAction
|
||||||
|
| IUserPassChangeAction
|
||||||
|
| IUserPassChangeFailAction
|
||||||
|
| IUserPassChangeSuccessAction;
|
||||||
39
frontend/src/redux/user/reducer.ts
Normal file
39
frontend/src/redux/user/reducer.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { Reducer } from "react";
|
||||||
|
import { IUserJSON } from "~../../src/entity/User";
|
||||||
|
import { AuthAction, AuthTypes } from "~redux/auth/actions";
|
||||||
|
import { UserAction, UserTypes } from "./actions";
|
||||||
|
|
||||||
|
export interface IUserState {
|
||||||
|
user: IUserJSON | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultUserState: IUserState = {
|
||||||
|
user: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const userReducer: Reducer<IUserState, AuthAction> = (
|
||||||
|
state: IUserState = defaultUserState,
|
||||||
|
action: AuthAction | UserAction,
|
||||||
|
): IUserState => {
|
||||||
|
switch (action.type) {
|
||||||
|
case AuthTypes.AUTH_SUCCESS:
|
||||||
|
case UserTypes.USER_GET_SUCCESS:
|
||||||
|
case UserTypes.USER_PASS_CHANGE_SUCCESS:
|
||||||
|
return {
|
||||||
|
...defaultUserState,
|
||||||
|
user: action.payload,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case UserTypes.USER_GET_FAIL:
|
||||||
|
case UserTypes.USER_PASS_CHANGE_FAIL:
|
||||||
|
return defaultUserState;
|
||||||
|
break;
|
||||||
|
case UserTypes.USER_LOGOUT:
|
||||||
|
return defaultUserState;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
};
|
||||||
62
frontend/src/redux/user/sagas.ts
Normal file
62
frontend/src/redux/user/sagas.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { all, call, delay, put, race, takeLatest } from "redux-saga/effects";
|
||||||
|
import { changeUserPassword, fetchUser } from "~redux/api/user";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getUserFail,
|
||||||
|
getUserSuccess,
|
||||||
|
IUserPassChangeAction,
|
||||||
|
userPassChangeFail,
|
||||||
|
userPassChangeSuccess,
|
||||||
|
UserTypes,
|
||||||
|
} from "./actions";
|
||||||
|
|
||||||
|
function* getUser() {
|
||||||
|
try {
|
||||||
|
const { response, timeout } = yield race({
|
||||||
|
response: call(fetchUser),
|
||||||
|
timeout: delay(10000),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (timeout) {
|
||||||
|
yield put(getUserFail("Timeout", false));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (response.data) {
|
||||||
|
const user = response.data;
|
||||||
|
yield put(getUserSuccess(user));
|
||||||
|
} else {
|
||||||
|
yield put(getUserFail(response.error, true));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
yield put(getUserFail("Internal error", false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function* userPassChange(action: IUserPassChangeAction) {
|
||||||
|
try {
|
||||||
|
const { response, timeout } = yield race({
|
||||||
|
response: call(changeUserPassword, action.password),
|
||||||
|
timeout: delay(10000),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (timeout) {
|
||||||
|
yield put(userPassChangeFail("Timeout", false));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (response.data) {
|
||||||
|
const user = response.data;
|
||||||
|
yield put(userPassChangeSuccess(user));
|
||||||
|
} else {
|
||||||
|
yield put(userPassChangeFail(response.error, true));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
yield put(userPassChangeFail("Internal error", false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function* userSaga() {
|
||||||
|
yield all([
|
||||||
|
takeLatest(UserTypes.USER_GET, getUser),
|
||||||
|
takeLatest(UserTypes.USER_PASS_CHANGE, userPassChange),
|
||||||
|
]);
|
||||||
|
}
|
||||||
1
frontend/src/styleMock.ts
Normal file
1
frontend/src/styleMock.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
module.exports = {};
|
||||||
3
frontend/src/tests/utils.ts
Normal file
3
frontend/src/tests/utils.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export function flushPromises() {
|
||||||
|
return new Promise(setImmediate);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user