Frontend config and Loader component (#12)

* Add FrontendConfig and Loader

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* Merge more setup into the application loader

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* Rename config files

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* Remove blank line

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* Fix url

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* Make strings more specific

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* Restructure store use

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* split methods and actions

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* extract code

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* remove actions.ts

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* add reason and rename component

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* remove unused call

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* Use redux store in api

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* activate email in backend config

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* add new line

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* reduce code

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* Make error more specific

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* Use expectedResponseCode

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* Update src/redux/backend-config/types.ts

Co-authored-by: Philip Molares <git@molar.es>

* Update src/components/application-loader/application-loader.tsx

Co-authored-by: Philip Molares <git@molar.es>

* Update src/components/application-loader/application-loader.tsx

Co-authored-by: Philip Molares <git@molar.es>

* Update src/components/application-loader/application-loader.tsx

Co-authored-by: Philip Molares <git@molar.es>

* Use fragment

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

Co-authored-by: Philip Molares <git@molar.es>
This commit is contained in:
mrdrogdrog 2020-05-15 23:10:12 +02:00 committed by GitHub
parent ef17c7acbb
commit a490e1240b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 425 additions and 256 deletions

3
public/config.json Normal file
View file

@ -0,0 +1,3 @@
{
"backendUrl": "http://localhost:3000"
}

View file

@ -1,3 +1,15 @@
export const getConfig = async () => {
return fetch('/config');
import {FrontendConfigState} from "../redux/frontend-config/types";
import {BackendConfigState} from "../redux/backend-config/types";
import {expectResponseCode, getBackendUrl} from "../utils/apiUtils";
export const getBackendConfig = async () => {
return fetch(getBackendUrl() + '/backend-config.json')
.then(expectResponseCode())
.then(response => response.json() as Promise<BackendConfigState>);
}
export const getFrontendConfig = async () => {
return fetch(getBackendUrl() + '/config.json')
.then(expectResponseCode())
.then(response => response.json() as Promise<FrontendConfigState>);
}

View file

@ -1,27 +1,25 @@
import {expectResponseCode, getBackendUrl} from "../utils/apiUtils";
export const getMe = async () => {
return fetch('/me');
return fetch('/me');
}
export const postEmailLogin = async (email: string, password: string) => {
return fetch("/login", {
method: 'POST',
mode: 'cors',
cache: 'no-cache',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json'
},
redirect: 'follow',
referrerPolicy: 'no-referrer',
body: JSON.stringify({
email: email,
password: password,
})
}).then(response => {
if (response.status !== 200) {
return Promise.reject("Response code not 200");
} else {
return response.json();
}
})
}
return fetch(getBackendUrl() + "/login", {
method: 'POST',
mode: 'cors',
cache: 'no-cache',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json'
},
redirect: 'follow',
referrerPolicy: 'no-referrer',
body: JSON.stringify({
email: email,
password: password,
})
})
.then(expectResponseCode())
.then(response => response.json());
}

View file

@ -0,0 +1,80 @@
.loader {
.animation-pulse {
animation: pulse 2s ease-in-out infinite;
}
.animation-shake {
animation: shake 0.3s ease-in-out;
}
.icon {
width: 150px;
height: 150px;
}
height: 100vh;
width: 100vw;
&.middle, .middle {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.progress {
width: 50%;
}
@keyframes pulse {
0% {
transform: scale(1, 1);
filter: drop-shadow(0 0 0px black);
}
10% {
transform: scale(1.5, 1.5);
filter: drop-shadow(0 0 100px white);
}
100% {
transform: scale(1, 1);
filter: drop-shadow(0 0 0px black);
}
}
@keyframes shake {
0% {
transform: translate(1px, 1px) rotate(0deg);
}
10% {
transform: translate(-1px, -2px) rotate(-1deg);
}
20% {
transform: translate(-3px, 0px) rotate(1deg);
}
30% {
transform: translate(3px, 2px) rotate(0deg);
}
40% {
transform: translate(1px, -1px) rotate(1deg);
}
50% {
transform: translate(-1px, 2px) rotate(-1deg);
}
60% {
transform: translate(-3px, 1px) rotate(0deg);
}
70% {
transform: translate(3px, 1px) rotate(-1deg);
}
80% {
transform: translate(-1px, -1px) rotate(1deg);
}
90% {
transform: translate(1px, 2px) rotate(0deg);
}
100% {
transform: translate(1px, -2px) rotate(-1deg);
}
}
}

View file

@ -0,0 +1,41 @@
import React, {Fragment, useEffect, useState} from "react";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import "./application-loader.scss";
import {Alert} from "react-bootstrap";
interface ApplicationLoaderProps {
initTasks: Promise<any>[]
}
export const ApplicationLoader: React.FC<ApplicationLoaderProps> = ({children, initTasks}) => {
const [failed, setFailed] = useState<boolean>(false);
const [doneTasks, setDoneTasks] = useState<number>(0);
useEffect(() => {
setDoneTasks(0);
initTasks.map(task =>
task.then(() =>
setDoneTasks(prevDoneTasks => {
return prevDoneTasks + 1;
}))
.catch((reason) => {
setFailed(true);
console.error(reason);
})
)
}, [initTasks]);
return (<Fragment>{
doneTasks < initTasks.length || initTasks.length === 0 ? (
<div className="loader middle">
<div className="icon">
<FontAwesomeIcon icon="file-alt" size="6x"
className={failed ? "animation-shake" : "animation-pulse"}/>
</div>
{
failed ? <Alert variant={"danger"}>An error occured while loading the application!</Alert> : null
}
</div>
) : children
}</Fragment>);
}

View file

@ -1,44 +0,0 @@
import {useDispatch} from "react-redux";
import React from "react";
import {getConfig} from "../../api/config";
import {ApplicationConfigState} from "../../redux/application-config/types";
import {setApplicationConfig} from "../../redux/application-config/actions";
const InitializeConfigStateFromApi: React.FC = () => {
const dispatch = useDispatch();
getConfig()
.then((response) => {
if (response.status === 200) {
return (response.json() as Promise<ApplicationConfigState>);
}
})
.then(config => {
if (!config) {
return;
}
dispatch(setApplicationConfig({
allowAnonymous: config.allowAnonymous,
authProviders: {
facebook: config.authProviders.facebook,
github: config.authProviders.github,
twitter: config.authProviders.twitter,
gitlab: config.authProviders.gitlab,
dropbox: config.authProviders.dropbox,
ldap: config.authProviders.ldap,
google: config.authProviders.google,
saml: config.authProviders.saml,
oauth2: config.authProviders.oauth2,
email: config.authProviders.email
},
specialLinks: {
privacy: config.specialLinks.privacy,
termsOfUse: config.specialLinks.termsOfUse,
imprint: config.specialLinks.imprint,
}
}));
});
return null;
}
export { InitializeConfigStateFromApi }

View file

@ -1,35 +0,0 @@
import {useDispatch} from "react-redux";
import {getMe} from "../../api/user";
import {setUser} from "../../redux/user/actions";
import {LoginStatus, UserState} from "../../redux/user/types";
import React from "react";
import {Dispatch} from "redux";
export const getAndSetUser = (dispatch: Dispatch<any>) => {
getMe()
.then((me) => {
if (me.status === 200) {
return (me.json() as Promise<UserState>);
}
})
.then(user => {
if (!user) {
return;
}
dispatch(setUser({
status: LoginStatus.ok,
id: user.id,
name: user.name,
photo: user.photo,
}));
});
}
const InitializeUserStateFromApi: React.FC = () => {
const dispatch = useDispatch();
getAndSetUser(dispatch);
return null;
}
export { InitializeUserStateFromApi }

View file

@ -18,7 +18,7 @@ const PoweredByLinks: React.FC = () => {
}
]
const config = useSelector((state: ApplicationState) => state.applicationConfig);
const config = useSelector((state: ApplicationState) => state.backendConfig);
const specialLinks = Object.entries(config.specialLinks)
.filter(([_, value]) => value !== "")

View file

@ -9,20 +9,19 @@ import "./style/index.scss";
import {Login} from "../pages/login/login";
export const Landing: React.FC = () => {
return (
<Container>
<HeaderBar/>
<Switch>
<Route path="/history">
<History/>
</Route>
<Route path="/intro">
<Intro/>
</Route>
<Route path="/login">
<Login/>
</Route>
</Switch>
<Footer/>
</Container>);
return (<Container>
<HeaderBar/>
<Switch>
<Route path="/history">
<History/>
</Route>
<Route path="/intro">
<Intro/>
</Route>
<Route path="/login">
<Login/>
</Route>
</Switch>
<Footer/>
</Container>)
}

View file

@ -1,4 +1,4 @@
import React from 'react'
import React, {Fragment} from 'react'
import {Navbar} from 'react-bootstrap';
import {useSelector} from "react-redux";
import {ApplicationState} from "../../../../../redux";
@ -27,19 +27,19 @@ const HeaderBar: React.FC = () => {
</div>
<div className="d-inline-flex">
{user.status === LoginStatus.forbidden ?
<>
<Fragment>
<span className={"mr-1"}>
<NewGuestNoteButton/>
</span>
<SignInButton/>
</>
</Fragment>
:
<>
<Fragment>
<span className={"mr-1"}>
<NewUserNoteButton/>
</span>
<UserDropdown/>
</>
</Fragment>
}
</div>
</Navbar>

View file

@ -1,16 +1,15 @@
import {Dropdown} from "react-bootstrap";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import React from "react";
import {useDispatch, useSelector} from "react-redux";
import {useSelector} from "react-redux";
import {ApplicationState} from "../../../../../redux";
import {LinkContainer} from "react-router-bootstrap";
import {clearUser} from "../../../../../redux/user/actions";
import {clearUser} from "../../../../../redux/user/methods";
import "./user-dropdown.scss";
import {Trans} from "react-i18next";
export const UserDropdown: React.FC = () => {
const user = useSelector((state: ApplicationState) => state.user);
const dispatch = useDispatch()
return (
<Dropdown alignRight>
@ -43,7 +42,7 @@ export const UserDropdown: React.FC = () => {
</Dropdown.Item>
<Dropdown.Item
onClick={() => {
dispatch(clearUser());
clearUser();
}}>
<FontAwesomeIcon icon="sign-out-alt"/>&nbsp;
<Trans i18nKey="signOut"/>

View file

@ -1,13 +1,11 @@
import {Trans, useTranslation} from "react-i18next";
import {Button, Form, Alert} from "react-bootstrap";
import {Alert, Button, Form} from "react-bootstrap";
import React, {Fragment, useState} from "react";
import {postEmailLogin} from "../../../../../api/user";
import {useDispatch} from "react-redux";
import {getAndSetUser} from "../../../../initialize/initialize-user-state-from-api";
import {getAndSetUser} from "../../../../../utils/apiUtils";
const ViaEMail: React.FC = () => {
const {t} = useTranslation();
const dispatch = useDispatch();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState(false);
@ -15,10 +13,11 @@ const ViaEMail: React.FC = () => {
postEmailLogin(email, password)
.then(loginJson => {
console.log(loginJson)
getAndSetUser(dispatch);
}).catch(_reason => {
getAndSetUser();
}).catch(_reason => {
setError(true);
})
}
)
event.preventDefault();
}
@ -55,8 +54,7 @@ const ViaEMail: React.FC = () => {
<Button
type="submit"
size="sm"
variant="primary"
>
variant="primary">
<Trans i18nKey="signIn"/>
</Button>
</Form>
@ -64,4 +62,4 @@ const ViaEMail: React.FC = () => {
);
}
export { ViaEMail }
export {ViaEMail}

View file

@ -9,7 +9,7 @@ import {ApplicationState} from "../../../../redux";
const Login: React.FC = () => {
useTranslation();
const authProviders = useSelector((state: ApplicationState) => state.applicationConfig.authProviders);
const authProviders = useSelector((state: ApplicationState) => state.backendConfig.authProviders);
const emailForm = authProviders.email ? <ViaEMail/> : null
const ldapForm = authProviders.ldap ? <ViaLdap/> : null

View file

@ -2,27 +2,23 @@ import React from 'react'
import ReactDOM from 'react-dom'
import {BrowserRouter as Router} from 'react-router-dom'
import * as serviceWorker from './service-worker';
import {ApplicationStateStoreProvider} from "./redux/application-state-store-provider/application-state-store-provider";
import {setUpI18n} from "./initializers/i18n";
import {InitializeUserStateFromApi} from "./components/initialize/initialize-user-state-from-api";
import {Landing} from "./components/landing/layout";
import {setUpFontAwesome} from "./initializers/fontAwesome";
import {InitializeConfigStateFromApi} from "./components/initialize/initalize-config-state-from-api";
import {ApplicationLoader} from "./components/application-loader/application-loader";
import {Provider} from "react-redux";
import {store} from "./utils/store";
import {setUp} from "./initializers";
setUpFontAwesome();
setUpI18n().then(
() => {
ReactDOM.render(
<ApplicationStateStoreProvider>
<InitializeUserStateFromApi/>
<InitializeConfigStateFromApi/>
<Router>
<Landing/>
</Router>
</ApplicationStateStoreProvider>
, document.getElementById('root')
)
}
const initTasks = setUp();
ReactDOM.render(
<Provider store={store}>
<ApplicationLoader initTasks={initTasks}>
<Router>
<Landing/>
</Router>
</ApplicationLoader>
</Provider>
, document.getElementById('root')
);
// If you want your app to work offline and load faster, you can change

View file

@ -0,0 +1,24 @@
import {getBackendConfig, getFrontendConfig} from "../api/config";
import {setFrontendConfig} from "../redux/frontend-config/methods";
import {setBackendConfig} from "../redux/backend-config/methods";
import {getAndSetUser} from "../utils/apiUtils";
export function loadAllConfig() {
return getFrontendConfig()
.then((frontendConfig) => {
if (!frontendConfig) {
return Promise.reject("Frontend config empty!");
}
setFrontendConfig(frontendConfig);
return getBackendConfig()
})
.then((backendConfig) => {
if (!backendConfig) {
return Promise.reject("Backend config empty!");
}
setBackendConfig(backendConfig)
}).then(() => {
getAndSetUser();
})
}

View file

@ -0,0 +1,8 @@
import {setUpFontAwesome} from "./fontAwesome";
import {setUpI18n} from "./i18n";
import {loadAllConfig} from "./configLoader";
export function setUp() {
setUpFontAwesome();
return [setUpI18n(), loadAllConfig()]
}

View file

@ -1,20 +0,0 @@
import {Action, ActionCreator} from 'redux';
import {ApplicationConfigState} from "./types";
export const SET_APPLICATION_CONFIG_ACTION_TYPE = 'config/set';
export interface SetApplicationConfigAction extends Action {
type: string;
payload: {
state: ApplicationConfigState;
};
}
export const setApplicationConfig: ActionCreator<SetApplicationConfigAction> = (state: ApplicationConfigState) => ({
type: SET_APPLICATION_CONFIG_ACTION_TYPE,
payload: {
state
},
})
export type ApplicationConfigActions = SetApplicationConfigAction;

View file

@ -1,13 +0,0 @@
import {createStore} from 'redux';
import {allReducers} from '../index';
import {Provider} from "react-redux";
import React, {ReactNode} from "react";
interface JustChildrenProps {
children: ReactNode;
}
export const ApplicationStateStoreProvider: React.FC<JustChildrenProps> = (props: JustChildrenProps) => {
const store = createStore(allReducers);
return <Provider store={store}>{props.children}</Provider>;
}

View file

@ -0,0 +1,12 @@
import {BackendConfigState, SET_BACKEND_CONFIG_ACTION_TYPE, SetBackendConfigAction} from "./types";
import {store} from "../../utils/store";
export const setBackendConfig = (state: BackendConfigState) => {
const action: SetBackendConfigAction = {
type: SET_BACKEND_CONFIG_ACTION_TYPE,
payload: {
state
}
};
store.dispatch(action)
}

View file

@ -1,8 +1,7 @@
import {Reducer} from 'redux';
import {ApplicationConfigState} from './types';
import {ApplicationConfigActions, SET_APPLICATION_CONFIG_ACTION_TYPE} from "./actions";
import {BackendConfigActions, BackendConfigState, SET_BACKEND_CONFIG_ACTION_TYPE} from './types';
export const initialState: ApplicationConfigState = {
export const initialState: BackendConfigState = {
allowAnonymous: true,
authProviders: {
facebook: true,
@ -23,9 +22,9 @@ export const initialState: ApplicationConfigState = {
}
};
export const ApplicationConfigReducer: Reducer<ApplicationConfigState, ApplicationConfigActions> = (state: ApplicationConfigState = initialState, action: ApplicationConfigActions) => {
export const BackendConfigReducer: Reducer<BackendConfigState, BackendConfigActions> = (state: BackendConfigState = initialState, action: BackendConfigActions) => {
switch (action.type) {
case SET_APPLICATION_CONFIG_ACTION_TYPE:
case SET_BACKEND_CONFIG_ACTION_TYPE:
return action.payload.state;
default:
return state;

View file

@ -1,10 +1,13 @@
export interface ApplicationConfigState {
import {Action} from "redux";
export const SET_BACKEND_CONFIG_ACTION_TYPE = 'backend-config/set';
export interface BackendConfigState {
allowAnonymous: boolean,
authProviders: AuthProvidersState,
specialLinks: SpecialLinks,
}
export interface AuthProvidersState {
facebook: true,
github: false,
@ -23,3 +26,12 @@ export interface SpecialLinks {
termsOfUse: string,
imprint: string,
}
export interface SetBackendConfigAction extends Action {
type: string;
payload: {
state: BackendConfigState;
};
}
export type BackendConfigActions = SetBackendConfigAction;

View file

@ -0,0 +1,12 @@
import {FrontendConfigState, SET_FRONTEND_CONFIG_ACTION_TYPE, SetFrontendConfigAction} from "./types";
import {store} from "../../utils/store";
export const setFrontendConfig = (state: FrontendConfigState) => {
const action: SetFrontendConfigAction = {
type: SET_FRONTEND_CONFIG_ACTION_TYPE,
payload: {
state
}
}
store.dispatch(action);
}

View file

@ -0,0 +1,15 @@
import {Reducer} from 'redux';
import {FrontendConfigActions, FrontendConfigState, SET_FRONTEND_CONFIG_ACTION_TYPE} from './types';
export const initialState: FrontendConfigState = {
backendUrl: ""
};
export const FrontendConfigReducer: Reducer<FrontendConfigState, FrontendConfigActions> = (state: FrontendConfigState = initialState, action: FrontendConfigActions) => {
switch (action.type) {
case SET_FRONTEND_CONFIG_ACTION_TYPE:
return action.payload.state;
default:
return state;
}
};

View file

@ -0,0 +1,16 @@
import {Action} from "redux";
export const SET_FRONTEND_CONFIG_ACTION_TYPE = 'frontend-config/set';
export interface SetFrontendConfigAction extends Action {
type: string;
payload: {
state: FrontendConfigState;
};
}
export interface FrontendConfigState {
backendUrl: string,
}
export type FrontendConfigActions = SetFrontendConfigAction;

View file

@ -1,19 +1,23 @@
import {combineReducers, Reducer} from 'redux';
import {UserState} from "./user/types";
import {UserReducer} from "./user/reducers";
import {ApplicationConfigReducer} from "./application-config/reducers";
import {ApplicationConfigState} from "./application-config/types";
import {ModalShowReducer} from "./modal/reducers";
import {ModalShowState} from "./modal/types";
import {BackendConfigState} from "./backend-config/types";
import {FrontendConfigState} from "./frontend-config/types";
import {BackendConfigReducer} from "./backend-config/reducers";
import {FrontendConfigReducer} from "./frontend-config/reducers";
export interface ApplicationState {
user: UserState;
modalShow: ModalShowState;
applicationConfig: ApplicationConfigState;
backendConfig: BackendConfigState;
frontendConfig: FrontendConfigState;
}
export const allReducers: Reducer<ApplicationState> = combineReducers<ApplicationState>({
user: UserReducer,
modalShow: ModalShowReducer,
applicationConfig: ApplicationConfigReducer
backendConfig: BackendConfigReducer,
frontendConfig: FrontendConfigReducer
});

View file

@ -1,15 +0,0 @@
import {Action, ActionCreator} from 'redux';
export const SET_HISTORY_DELETE_MODAL_SHOW_ACTION_TYPE = 'modal/history-delete/set';
export interface SetHistoryDeleteModalShowAction extends Action {
type: string;
payload: boolean;
}
export const setSignInModalShow: ActionCreator<SetHistoryDeleteModalShowAction> = (historyDelete: boolean) => ({
type: SET_HISTORY_DELETE_MODAL_SHOW_ACTION_TYPE,
payload: historyDelete,
})
export type ModalShowActions = SetHistoryDeleteModalShowAction;

View file

@ -0,0 +1,7 @@
import {ActionCreator} from "redux";
import {SET_HISTORY_DELETE_MODAL_SHOW_ACTION_TYPE, SetHistoryDeleteModalShowAction} from "./types";
export const setSignInModalShow: ActionCreator<SetHistoryDeleteModalShowAction> = (historyDelete: boolean) => ({
type: SET_HISTORY_DELETE_MODAL_SHOW_ACTION_TYPE,
payload: historyDelete,
})

View file

@ -1,6 +1,5 @@
import {Reducer} from 'redux';
import {ModalShowState} from './types';
import {ModalShowActions, SET_HISTORY_DELETE_MODAL_SHOW_ACTION_TYPE} from "./actions";
import {ModalShowActions, ModalShowState, SET_HISTORY_DELETE_MODAL_SHOW_ACTION_TYPE} from './types';
export const initialState: ModalShowState = {
historyDelete: false

View file

@ -1,3 +1,14 @@
import {Action} from "redux";
export const SET_HISTORY_DELETE_MODAL_SHOW_ACTION_TYPE = 'modal/history-delete/set';
export interface ModalShowState {
historyDelete: boolean
}
export interface SetHistoryDeleteModalShowAction extends Action {
type: string;
payload: boolean;
}
export type ModalShowActions = SetHistoryDeleteModalShowAction;

View file

@ -1,32 +0,0 @@
import {Action, ActionCreator} from 'redux';
import {UserState} from "./types";
export const SET_USER_ACTION_TYPE = 'user/set';
export const CLEAR_USER_ACTION_TYPE = 'user/clear';
export interface SetUserAction extends Action {
type: string;
payload: {
state: UserState,
};
}
export interface ClearUserAction extends Action {
type: string;
payload: {};
}
export const setUser: ActionCreator<SetUserAction> = (state: UserState) => ({
type: SET_USER_ACTION_TYPE,
payload: {
state
},
})
export const clearUser: ActionCreator<ClearUserAction> = () => ({
type: CLEAR_USER_ACTION_TYPE,
payload: {},
})
export type UserActions = SetUserAction | ClearUserAction;

20
src/redux/user/methods.ts Normal file
View file

@ -0,0 +1,20 @@
import {CLEAR_USER_ACTION_TYPE, ClearUserAction, SET_USER_ACTION_TYPE, SetUserAction, UserState} from "./types";
import {store} from "../../utils/store";
export const setUser = (state: UserState) => {
const action: SetUserAction = {
type: SET_USER_ACTION_TYPE,
payload: {
state
}
}
store.dispatch(action);
}
export const clearUser = () => {
const action: ClearUserAction = {
type: CLEAR_USER_ACTION_TYPE,
payload: {},
}
store.dispatch(action);
}

View file

@ -1,6 +1,12 @@
import {Reducer} from 'redux';
import {LoginStatus, UserState} from './types';
import {CLEAR_USER_ACTION_TYPE, SET_USER_ACTION_TYPE, SetUserAction, UserActions} from "./actions";
import {
CLEAR_USER_ACTION_TYPE,
LoginStatus,
SET_USER_ACTION_TYPE,
SetUserAction,
UserActions,
UserState
} from './types';
export const initialState: UserState = {
id: "",

View file

@ -1,3 +1,20 @@
import {Action} from "redux";
export const SET_USER_ACTION_TYPE = 'user/set';
export const CLEAR_USER_ACTION_TYPE = 'user/clear';
export interface SetUserAction extends Action {
type: string;
payload: {
state: UserState,
};
}
export interface ClearUserAction extends Action {
type: string;
payload: {};
}
export interface UserState {
status: LoginStatus;
id: string;
@ -8,4 +25,6 @@ export interface UserState {
export enum LoginStatus {
forbidden = "forbidden",
ok = "ok"
}
}
export type UserActions = SetUserAction | ClearUserAction;

34
src/utils/apiUtils.ts Normal file
View file

@ -0,0 +1,34 @@
import {getMe} from "../api/user";
import {LoginStatus} from "../redux/user/types";
import {setUser} from "../redux/user/methods";
import {store} from "./store";
export const getAndSetUser = () => {
getMe()
.then(expectResponseCode())
.then(user => {
if (!user) {
return;
}
setUser({
status: LoginStatus.ok,
id: user.id,
name: user.name,
photo: user.photo,
});
});
}
export const getBackendUrl = () => {
return store.getState().frontendConfig.backendUrl;
}
export const expectResponseCode = (code: number = 200): ((response: Response) => Promise<any>) => {
return (response: Response) => {
if (response.status !== code) {
return Promise.reject(`Response code not ${code}`);
} else {
return Promise.resolve(response);
}
}
}

4
src/utils/store.ts Normal file
View file

@ -0,0 +1,4 @@
import {createStore} from "redux";
import {allReducers} from "../redux";
export const store = createStore(allReducers);