Initial commit
This commit is contained in:
commit
fa30c04815
117 changed files with 33513 additions and 0 deletions
42
src/services/api/apiCall.ts
Normal file
42
src/services/api/apiCall.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import _ from 'lodash';
|
||||
import axios from 'axios';
|
||||
|
||||
import { api } from './config';
|
||||
import { ApiConfig } from './types';
|
||||
|
||||
/**
|
||||
* Function which creates an API call for given api config
|
||||
*/
|
||||
export const createApiCall = (apiConfig: ApiConfig) => async () => {
|
||||
const hostname = apiConfig.hostname || api.hostname;
|
||||
const { path } = apiConfig;
|
||||
const method = _.get(apiConfig, 'method', 'GET');
|
||||
const contentType =
|
||||
_.includes(['POST', 'PUT', 'PATCH'], method) && !apiConfig.formData
|
||||
? {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
: {};
|
||||
const passedHeaders = _.get(apiConfig, 'headers', {});
|
||||
const headers = {
|
||||
...contentType,
|
||||
...passedHeaders,
|
||||
};
|
||||
const axiosConfig = {
|
||||
method,
|
||||
headers,
|
||||
url: `${hostname}${path}`,
|
||||
timeout: 30000,
|
||||
data: apiConfig.formData || apiConfig.body,
|
||||
};
|
||||
|
||||
return axios(axiosConfig);
|
||||
};
|
||||
|
||||
/**
|
||||
* Performs an API call for given api config and returns a promise
|
||||
*/
|
||||
export const performApiCall = (apiConfig: ApiConfig) => {
|
||||
const apiCall = createApiCall(apiConfig);
|
||||
return apiCall();
|
||||
};
|
||||
3
src/services/api/config.ts
Normal file
3
src/services/api/config.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export const api = {
|
||||
hostname: process.env.REACT_APP_API_URL,
|
||||
};
|
||||
5
src/services/api/index.ts
Normal file
5
src/services/api/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export * from './redux';
|
||||
|
||||
export { api } from './config';
|
||||
|
||||
export { createApiCall, performApiCall } from './apiCall';
|
||||
186
src/services/api/redux/actions.ts
Normal file
186
src/services/api/redux/actions.ts
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
import _ from 'lodash';
|
||||
import urlcat from 'urlcat';
|
||||
|
||||
import { createApiCall } from '../apiCall';
|
||||
import { StartAction, SuccessAction, FailureAction } from './types';
|
||||
|
||||
import { ApiConfig } from '../types';
|
||||
|
||||
/**
|
||||
* Function which creates a thunk action for given api config and associated action types
|
||||
* @param {string|Record<string, unknown>} apiConfig if string it is interprated as path in api endpoint
|
||||
* @param {string[]} actionTypes action types for start, success and failure
|
||||
*/
|
||||
export const createApiAction = (apiConfig: ApiConfig, actionTypes: string[]) => async (dispatch: any) => {
|
||||
const apiCall = createApiCall(apiConfig);
|
||||
|
||||
const startAction = (): StartAction => ({
|
||||
type: actionTypes[0],
|
||||
payload: null,
|
||||
});
|
||||
|
||||
const successAction = (payload: any): SuccessAction => ({
|
||||
type: actionTypes[1],
|
||||
payload,
|
||||
});
|
||||
|
||||
const failureAction = (error: string): FailureAction => ({
|
||||
type: actionTypes[2],
|
||||
payload: { error },
|
||||
});
|
||||
|
||||
dispatch(startAction());
|
||||
|
||||
try {
|
||||
const response = await apiCall();
|
||||
|
||||
let res;
|
||||
const additionalData = apiConfig.additionalData || {};
|
||||
|
||||
if (!_.isEmpty(additionalData)) {
|
||||
res = await dispatch(successAction({ ...response.data, ...additionalData }));
|
||||
} else {
|
||||
res = await dispatch(successAction(response.data));
|
||||
}
|
||||
|
||||
return { ok: true, res };
|
||||
} catch (e) {
|
||||
const error = _.get(e, 'response.data', {
|
||||
errorMessage: e.message || 'Undefined error, please try again.',
|
||||
});
|
||||
|
||||
await dispatch(failureAction(error));
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
errorMessage: error.message,
|
||||
status: e?.response?.status,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates opinionated CRUD actions
|
||||
*/
|
||||
export function createCrudApiActions<T>(
|
||||
basePath: string,
|
||||
startActionType: string,
|
||||
failureActionType: string,
|
||||
fetchActionType: string,
|
||||
addActionType: string,
|
||||
updateActionType: string,
|
||||
deleteActionType: string,
|
||||
transformItemForApi: (data: T) => any,
|
||||
pathTemplates: { load?: string; add?: string; change?: string } = {
|
||||
load: '/',
|
||||
add: '/',
|
||||
change: '/:id',
|
||||
},
|
||||
): any {
|
||||
const { load, add, change } = pathTemplates;
|
||||
const fetchItems = (
|
||||
pageNumber?: number,
|
||||
pageSize?: number,
|
||||
params: Record<string, unknown> = {},
|
||||
template: string = load!,
|
||||
) =>
|
||||
createApiAction(
|
||||
{
|
||||
path: urlcat(basePath, template, { pageNumber, pageSize, ...params }),
|
||||
method: 'GET',
|
||||
},
|
||||
[startActionType, fetchActionType, failureActionType],
|
||||
);
|
||||
|
||||
const addItem = (item: T, params: Record<string, unknown> = {}) =>
|
||||
createApiAction(
|
||||
{
|
||||
path: urlcat(basePath, add!, { ...params }),
|
||||
method: 'POST',
|
||||
body: transformItemForApi(item),
|
||||
},
|
||||
[startActionType, addActionType, failureActionType],
|
||||
);
|
||||
|
||||
const updateItem = (item: T, params: Record<string, unknown> = {}) =>
|
||||
createApiAction(
|
||||
{
|
||||
// @ts-ignore
|
||||
path: urlcat(basePath, change!, { id: item.id, ...params }),
|
||||
method: 'PUT',
|
||||
body: transformItemForApi(item),
|
||||
},
|
||||
[startActionType, updateActionType, failureActionType],
|
||||
);
|
||||
|
||||
const deleteItem = (itemId: number, params: Record<string, unknown> = {}) =>
|
||||
createApiAction(
|
||||
{
|
||||
path: urlcat(basePath, change!, { id: itemId, ...params }),
|
||||
method: 'DELETE',
|
||||
},
|
||||
[startActionType, deleteActionType, failureActionType],
|
||||
);
|
||||
|
||||
return [fetchItems, addItem, updateItem, deleteItem];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates opinionated CRUD actions
|
||||
*/
|
||||
export function createCrudApiActionsWithoutPaging<T extends { id?: number | string | null }>(
|
||||
basePath: string,
|
||||
startActionType: string,
|
||||
failureActionType: string,
|
||||
fetchActionType: string,
|
||||
addActionType: string,
|
||||
updateActionType: string,
|
||||
deleteActionType: string,
|
||||
transformItemForApi: (data: T) => any,
|
||||
pathTemplates: { load?: string; add?: string; change?: string } = {
|
||||
load: '/',
|
||||
add: '/',
|
||||
change: '/:id',
|
||||
},
|
||||
): any {
|
||||
const { load, add, change } = pathTemplates;
|
||||
const fetchItems = (params: Record<string, unknown> = {}, template: string = load!) =>
|
||||
createApiAction(
|
||||
{
|
||||
path: urlcat(basePath, template, params),
|
||||
method: 'GET',
|
||||
},
|
||||
[startActionType, fetchActionType, failureActionType],
|
||||
);
|
||||
|
||||
const addItem = (item: T, params: Record<string, unknown> = {}) =>
|
||||
createApiAction(
|
||||
{
|
||||
path: urlcat(basePath, add!, { ...params }),
|
||||
method: 'POST',
|
||||
body: transformItemForApi(item),
|
||||
},
|
||||
[startActionType, addActionType, failureActionType],
|
||||
);
|
||||
|
||||
const updateItem = (item: T, params: Record<string, unknown> = {}) =>
|
||||
createApiAction(
|
||||
{
|
||||
path: urlcat(basePath, change!, { id: item.id, ...params }),
|
||||
method: 'PUT',
|
||||
body: transformItemForApi(item),
|
||||
},
|
||||
[startActionType, updateActionType, failureActionType],
|
||||
);
|
||||
|
||||
const deleteItem = (itemId: number, params: Record<string, unknown> = {}) =>
|
||||
createApiAction(
|
||||
{
|
||||
path: urlcat(basePath, change!, { id: itemId, ...params }),
|
||||
method: 'DELETE',
|
||||
},
|
||||
[startActionType, deleteActionType, failureActionType],
|
||||
);
|
||||
|
||||
return [fetchItems, addItem, updateItem, deleteItem];
|
||||
}
|
||||
14
src/services/api/redux/index.ts
Normal file
14
src/services/api/redux/index.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
export {
|
||||
createApiReducer,
|
||||
createCrudApiReducer,
|
||||
createCrudApiReducerWithoutPaging,
|
||||
chainReducers,
|
||||
INITIAL_API_STATUS,
|
||||
INITIAL_API_STATE,
|
||||
} from './reducers';
|
||||
|
||||
export { createApiAction, createCrudApiActions, createCrudApiActionsWithoutPaging } from './actions';
|
||||
|
||||
export { isEmpty, isLoading, isValid, isError, getError, getErrorMessage } from './traits';
|
||||
|
||||
export * from './types';
|
||||
176
src/services/api/redux/reducers.ts
Normal file
176
src/services/api/redux/reducers.ts
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
import { StartAction, SuccessAction, FailureAction, ApiStatus, ApiState } from './types';
|
||||
|
||||
export const INITIAL_API_STATUS: ApiStatus = {
|
||||
isEmpty: true,
|
||||
isLoading: false,
|
||||
isValid: false,
|
||||
isError: false,
|
||||
dateTime: Date.now(),
|
||||
error: null,
|
||||
errorMessage: null,
|
||||
};
|
||||
|
||||
export const INITIAL_API_STATE: ApiState = {
|
||||
_status: INITIAL_API_STATUS,
|
||||
items: [],
|
||||
};
|
||||
|
||||
type AllActions = StartAction | SuccessAction | FailureAction;
|
||||
|
||||
/**
|
||||
* Use this function to create any reducer which handles api communication
|
||||
* @param {Array} actionTypes Array of action types associated with fetchThunk e.g. USER_REQUEST,
|
||||
* USER_SUCCESS and USER_ERROR
|
||||
* @param {function} dataAdapter function which will be called to transform response body
|
||||
* to format you want to use in state
|
||||
* @param {function} errorAdapter function which will be called to transform error
|
||||
* to format you want to use in state
|
||||
* @returns {function} reducer handles api requests, saves response body as data and status
|
||||
*/
|
||||
export const createApiReducer =
|
||||
(
|
||||
actionTypes: string[],
|
||||
dataAdapter: (data: any, state?: any) => any = (data: any) => data,
|
||||
errorAdapter = (data: any) => data,
|
||||
) =>
|
||||
(state = INITIAL_API_STATE, action: AllActions) => {
|
||||
const [REQUEST, SUCCESS, ERROR] = actionTypes;
|
||||
|
||||
switch (action.type) {
|
||||
case REQUEST:
|
||||
return {
|
||||
...state,
|
||||
_status: {
|
||||
...state._status,
|
||||
isLoading: true,
|
||||
dateTime: Date.now(),
|
||||
},
|
||||
};
|
||||
case SUCCESS:
|
||||
return {
|
||||
_status: {
|
||||
isEmpty: false,
|
||||
isLoading: false,
|
||||
isValid: true,
|
||||
isError: false,
|
||||
dateTime: Date.now(),
|
||||
error: null,
|
||||
errorMessage: null,
|
||||
},
|
||||
...dataAdapter(action.payload, state),
|
||||
};
|
||||
case ERROR:
|
||||
return {
|
||||
...state,
|
||||
_status: {
|
||||
isEmpty: false,
|
||||
isLoading: false,
|
||||
isValid: false,
|
||||
isError: true,
|
||||
error: action.payload,
|
||||
errorMessage: errorAdapter(action.payload),
|
||||
},
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This is higher order reducer
|
||||
* Use this if you want your state is combination of results of multiple reducers
|
||||
* under the same key, e.g. you can get the same data from multiple endpoints
|
||||
*/
|
||||
export const chainReducers =
|
||||
(initialState: any, ...args: any[]) =>
|
||||
(state = initialState, action: any) =>
|
||||
args.reduce((newState, reducer) => reducer(newState, action), state);
|
||||
|
||||
/**
|
||||
* Creates an opinionated CRUD reducer
|
||||
*/
|
||||
export function createCrudApiReducer<T>(
|
||||
startActionType: string,
|
||||
failureActionType: string,
|
||||
fetchActionType: string,
|
||||
addActionType: string,
|
||||
updateActionType: string,
|
||||
deleteActionType: string,
|
||||
transformItem: (data: any) => T,
|
||||
keyField = 'id',
|
||||
) {
|
||||
const fetchReducer = createApiReducer(
|
||||
[startActionType, fetchActionType, failureActionType],
|
||||
(data) => ({ _meta: data.meta, items: (data.data || data).map(transformItem) }),
|
||||
(data) => data,
|
||||
);
|
||||
|
||||
const addReducer = createApiReducer(
|
||||
[startActionType, addActionType, failureActionType],
|
||||
(data, state) => ({ items: [...state.items, transformItem(data)] }),
|
||||
(data) => data,
|
||||
);
|
||||
|
||||
const updateReducer = createApiReducer(
|
||||
[startActionType, updateActionType, failureActionType],
|
||||
(data, state) => ({
|
||||
items: state.items.map((i: any) => (i[keyField] === data[keyField] ? transformItem(data) : i)),
|
||||
}),
|
||||
(data) => data,
|
||||
);
|
||||
|
||||
const deleteReducer = createApiReducer(
|
||||
[startActionType, deleteActionType, failureActionType],
|
||||
(data, state) => ({
|
||||
items: state.items.filter((i: any) => i[keyField] !== data[keyField]),
|
||||
}),
|
||||
(data) => data,
|
||||
);
|
||||
|
||||
return chainReducers(INITIAL_API_STATE, fetchReducer, addReducer, updateReducer, deleteReducer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an opinionated CRUD reducer without paging
|
||||
*/
|
||||
export function createCrudApiReducerWithoutPaging<T>(
|
||||
startActionType: string,
|
||||
failureActionType: string,
|
||||
fetchActionType: string,
|
||||
addActionType: string,
|
||||
updateActionType: string,
|
||||
deleteActionType: string,
|
||||
transformItem: (data: any) => T,
|
||||
keyField = 'id',
|
||||
) {
|
||||
const fetchReducer = createApiReducer(
|
||||
[startActionType, fetchActionType, failureActionType],
|
||||
(data) => ({ items: data.map(transformItem) }),
|
||||
(data) => data,
|
||||
);
|
||||
|
||||
const addReducer = createApiReducer(
|
||||
[startActionType, addActionType, failureActionType],
|
||||
(data, state) => ({ items: [...state.items, transformItem(data)] }),
|
||||
(data) => data,
|
||||
);
|
||||
|
||||
const updateReducer = createApiReducer(
|
||||
[startActionType, updateActionType, failureActionType],
|
||||
(data, state) => ({
|
||||
items: state.items.map((i: any) => (i[keyField] === data[keyField] ? transformItem(data) : i)),
|
||||
}),
|
||||
(data) => data,
|
||||
);
|
||||
|
||||
const deleteReducer = createApiReducer(
|
||||
[startActionType, deleteActionType, failureActionType],
|
||||
(data, state) => ({
|
||||
items: state.items.filter((i: any) => i[keyField] !== data[keyField]),
|
||||
}),
|
||||
(data) => data,
|
||||
);
|
||||
|
||||
return chainReducers(INITIAL_API_STATE, fetchReducer, addReducer, updateReducer, deleteReducer);
|
||||
}
|
||||
29
src/services/api/redux/traits.ts
Normal file
29
src/services/api/redux/traits.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import _ from 'lodash';
|
||||
|
||||
export const isEmpty = (data: any): boolean => {
|
||||
return _.get(data, '_status.isEmpty', true);
|
||||
};
|
||||
|
||||
export const isLoading = (data: any): boolean => {
|
||||
return _.get(data, '_status.isLoading', false);
|
||||
};
|
||||
|
||||
export const isValid = (data: any): boolean => {
|
||||
return _.get(data, '_status.isValid', false);
|
||||
};
|
||||
|
||||
export const isError = (data: any): boolean => {
|
||||
return _.get(data, '_status.isError', false);
|
||||
};
|
||||
|
||||
export const getError = (data: any): any => {
|
||||
return _.get(data, '_status.error', null);
|
||||
};
|
||||
|
||||
export const getErrorMessage = (data: any): string | null => {
|
||||
return _.get(data, '_status.errorMessage', null);
|
||||
};
|
||||
|
||||
export const getTotalPages = (data: any): number | undefined => {
|
||||
return _.get(data, '_meta.totalPages', undefined);
|
||||
};
|
||||
29
src/services/api/redux/types.ts
Normal file
29
src/services/api/redux/types.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
export interface StartAction {
|
||||
type: string;
|
||||
payload: any;
|
||||
}
|
||||
|
||||
export interface SuccessAction {
|
||||
type: string;
|
||||
payload: any;
|
||||
}
|
||||
|
||||
export interface FailureAction {
|
||||
type: string;
|
||||
payload: { error: string };
|
||||
}
|
||||
|
||||
export interface ApiStatus {
|
||||
isEmpty: boolean;
|
||||
isLoading: boolean;
|
||||
isValid: boolean;
|
||||
isError: boolean;
|
||||
dateTime: number;
|
||||
error: any;
|
||||
errorMessage: string | null;
|
||||
}
|
||||
|
||||
export interface ApiState<T = any> {
|
||||
_status: ApiStatus;
|
||||
items: T[];
|
||||
}
|
||||
29
src/services/api/types.ts
Normal file
29
src/services/api/types.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { Method } from 'axios';
|
||||
|
||||
export interface ApiConfig {
|
||||
hostname?: string;
|
||||
path: string;
|
||||
method?: Method;
|
||||
headers?: any;
|
||||
formData?: any;
|
||||
body?: any;
|
||||
additionalData?: any;
|
||||
}
|
||||
|
||||
export interface SimpleSnackBarConfig {
|
||||
serviceName: any;
|
||||
}
|
||||
|
||||
export interface SnackBarConfigWithMessages {
|
||||
getMessage: {
|
||||
success(response: any): string;
|
||||
error(error: any): string;
|
||||
};
|
||||
}
|
||||
|
||||
export type SnackBarConfig = SnackBarConfigWithMessages | SimpleSnackBarConfig;
|
||||
|
||||
export enum SortDirection {
|
||||
Ascending = 'asc',
|
||||
Descending = 'desc',
|
||||
}
|
||||
42
src/services/auth/api.ts
Normal file
42
src/services/auth/api.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { performApiCall } from 'src/services/api';
|
||||
|
||||
export const sendPasswordResetLink = async (email: string): Promise<boolean> => {
|
||||
try {
|
||||
await performApiCall({
|
||||
path: `/auth/forgot-password`,
|
||||
method: 'POST',
|
||||
body: { email },
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const resetPassword = async (email: string, code: string, password: string): Promise<boolean> => {
|
||||
try {
|
||||
await performApiCall({
|
||||
path: `/auth/reset-password`,
|
||||
method: 'POST',
|
||||
body: { email, code, password },
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
export const verifyEmail = async (email: string, activationCode: string): Promise<boolean> => {
|
||||
try {
|
||||
await performApiCall({
|
||||
path: `/auth/activation`,
|
||||
method: 'POST',
|
||||
body: { email, activationCode },
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
1
src/services/auth/hooks/index.ts
Normal file
1
src/services/auth/hooks/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { useAuth } from './use-auth';
|
||||
38
src/services/auth/hooks/use-auth.ts
Normal file
38
src/services/auth/hooks/use-auth.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { getAuth, signIn, signOut, register as apiRegister, refreshUser as apiRefreshUser } from '../redux';
|
||||
|
||||
export function useAuth() {
|
||||
const dispatch = useDispatch();
|
||||
const auth = useSelector(getAuth);
|
||||
|
||||
const logIn = useCallback(
|
||||
(email: string, password: string) => {
|
||||
return dispatch(signIn(email, password));
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const register = useCallback(
|
||||
(email: string, password: string, firstName: string, lastName: string) => {
|
||||
return dispatch(apiRegister(email, password, firstName, lastName));
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const logOut = useCallback(() => {
|
||||
return dispatch(signOut());
|
||||
}, [dispatch]);
|
||||
|
||||
const refreshUser = useCallback(() => {
|
||||
return dispatch(apiRefreshUser());
|
||||
}, [dispatch]);
|
||||
|
||||
return {
|
||||
auth,
|
||||
logIn,
|
||||
logOut,
|
||||
register,
|
||||
refreshUser,
|
||||
};
|
||||
}
|
||||
3
src/services/auth/index.ts
Normal file
3
src/services/auth/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export { useAuth } from './hooks';
|
||||
export { getAuth, reducer, signIn, signOut, register, AuthActionTypes, getIsAuthLoading } from './redux';
|
||||
export * from './types';
|
||||
56
src/services/auth/redux/actions.ts
Normal file
56
src/services/auth/redux/actions.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { createApiAction } from 'src/services/api';
|
||||
import { SuccessAction } from 'src/services/api/redux/types';
|
||||
|
||||
export enum AuthActionTypes {
|
||||
SIGN_IN_START = 'auth/sign_in_start',
|
||||
SIGN_IN_SUCCESS = 'auth/sign_in_success',
|
||||
SIGN_IN_FAILURE = 'auth/sign_in_failure',
|
||||
SIGN_OUT = 'auth/SIGN_OUT',
|
||||
REGISTRATION_START = 'auth/registration_start',
|
||||
REGISTRATION_FAILURE = 'auth/registration_failure',
|
||||
}
|
||||
|
||||
const signOutAction = (): SuccessAction => ({
|
||||
type: AuthActionTypes.SIGN_OUT,
|
||||
payload: null,
|
||||
});
|
||||
|
||||
export const signIn = (email: string, password: string) =>
|
||||
createApiAction(
|
||||
{
|
||||
path: '/login',
|
||||
method: 'POST',
|
||||
body: { username: email, password },
|
||||
},
|
||||
[AuthActionTypes.SIGN_IN_START, AuthActionTypes.SIGN_IN_SUCCESS, AuthActionTypes.SIGN_IN_FAILURE],
|
||||
);
|
||||
|
||||
export const refreshUser = () =>
|
||||
createApiAction(
|
||||
{
|
||||
path: '/dashboard',
|
||||
method: 'GET',
|
||||
},
|
||||
[AuthActionTypes.SIGN_IN_START, AuthActionTypes.SIGN_IN_SUCCESS, AuthActionTypes.SIGN_IN_FAILURE],
|
||||
);
|
||||
|
||||
export function signOut() {
|
||||
return (dispatch: any) => {
|
||||
dispatch(signOutAction());
|
||||
};
|
||||
}
|
||||
|
||||
export const register = (email: string, password: string, firstName: string, lastName: string) =>
|
||||
createApiAction(
|
||||
{
|
||||
path: '/auth/register',
|
||||
method: 'POST',
|
||||
body: {
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
password,
|
||||
},
|
||||
},
|
||||
[AuthActionTypes.REGISTRATION_START, AuthActionTypes.SIGN_IN_SUCCESS, AuthActionTypes.REGISTRATION_FAILURE],
|
||||
);
|
||||
4
src/services/auth/redux/index.ts
Normal file
4
src/services/auth/redux/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export { signIn, signOut, register, refreshUser, AuthActionTypes } from './actions';
|
||||
export { default as reducer } from './reducers';
|
||||
export { getAuth, getIsAuthLoading } from './selectors';
|
||||
export * from './types';
|
||||
23
src/services/auth/redux/reducers.ts
Normal file
23
src/services/auth/redux/reducers.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { createApiReducer, chainReducers, INITIAL_API_STATUS, INITIAL_API_STATE } from 'src/services/api';
|
||||
|
||||
import { AuthState } from './types';
|
||||
import { AuthActionTypes } from './actions';
|
||||
|
||||
const initialState: AuthState = {
|
||||
token: null,
|
||||
_status: INITIAL_API_STATUS,
|
||||
};
|
||||
|
||||
const auth = createApiReducer(
|
||||
[AuthActionTypes.SIGN_IN_START, AuthActionTypes.SIGN_IN_SUCCESS, AuthActionTypes.SIGN_IN_FAILURE],
|
||||
(data) => ({ token: data.access_token }),
|
||||
(data) => data.error.message,
|
||||
);
|
||||
|
||||
const signOut = createApiReducer(
|
||||
['', AuthActionTypes.SIGN_OUT, ''],
|
||||
() => INITIAL_API_STATE,
|
||||
() => INITIAL_API_STATE,
|
||||
);
|
||||
|
||||
export default chainReducers(initialState, auth, signOut);
|
||||
9
src/services/auth/redux/selectors.ts
Normal file
9
src/services/auth/redux/selectors.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { State } from 'src/redux';
|
||||
|
||||
import { isLoading } from 'src/services/api';
|
||||
|
||||
export const getAuth = (state: State) => state.auth;
|
||||
|
||||
export const getIsAuthLoading = (state: State) => isLoading(getAuth(state));
|
||||
|
||||
export const getToken = (state: State) => state.auth.token;
|
||||
7
src/services/auth/redux/types.ts
Normal file
7
src/services/auth/redux/types.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { ApiStatus } from 'src/services/api/redux/types';
|
||||
|
||||
import { Auth } from '../types';
|
||||
|
||||
export interface AuthState extends Auth {
|
||||
_status: ApiStatus;
|
||||
}
|
||||
3
src/services/auth/types.ts
Normal file
3
src/services/auth/types.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export interface Auth {
|
||||
token: string | null;
|
||||
}
|
||||
11
src/services/users/api.ts
Normal file
11
src/services/users/api.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { performApiCall } from 'src/services/api';
|
||||
|
||||
import { User } from './types';
|
||||
import { transformUser } from './transformations';
|
||||
|
||||
export const fetchMemberDetails = async (memberId: number): Promise<User> => {
|
||||
const res = await performApiCall({
|
||||
path: `/members/${memberId}`,
|
||||
});
|
||||
return transformUser(res.data);
|
||||
};
|
||||
1
src/services/users/hooks/index.ts
Normal file
1
src/services/users/hooks/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { useUsers } from './use-users';
|
||||
16
src/services/users/hooks/use-users.ts
Normal file
16
src/services/users/hooks/use-users.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { getUsers, fetchUsers } from '../redux';
|
||||
|
||||
export function useUsers() {
|
||||
const dispatch = useDispatch();
|
||||
const users = useSelector(getUsers);
|
||||
|
||||
function loadUsers() {
|
||||
return dispatch(fetchUsers());
|
||||
}
|
||||
|
||||
return {
|
||||
users,
|
||||
loadUsers,
|
||||
};
|
||||
}
|
||||
7
src/services/users/index.ts
Normal file
7
src/services/users/index.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export * from './types';
|
||||
|
||||
export { reducer, fetchCurrentUser, getCurrentUser } from './redux';
|
||||
|
||||
export { useUsers } from './hooks';
|
||||
|
||||
export { fetchMemberDetails } from './api';
|
||||
64
src/services/users/redux/actions.ts
Normal file
64
src/services/users/redux/actions.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import { createApiAction, createCrudApiActions } from 'src/services/api';
|
||||
import { transformUserForApi } from '../transformations';
|
||||
|
||||
import { User } from '../types';
|
||||
import { CurrentUserUpdateAPI } from './types';
|
||||
|
||||
export enum UserActionTypes {
|
||||
CHANGE_CURRENT_USER_START = 'users/fetch_current_user_start',
|
||||
CHANGE_CURRENT_USER_FAILURE = 'users/fetch_current_user_failure',
|
||||
FETCH_CURRENT_USER_SUCCESS = 'users/fetch_current_user_success',
|
||||
UPDATE_CURRENT_USER_SUCCESS = 'users/update_current_user_success',
|
||||
|
||||
CHANGE_USERS_START = 'users/change_users_start',
|
||||
CHANGE_USERS_FAILURE = 'users/change_users_failure',
|
||||
FETCH_USERS_SUCCESS = 'users/fetch_users_success',
|
||||
ADD_ACCOUNT_USER_SUCCESS = 'users/add_account_user_success',
|
||||
UPDATE_ACCOUNT_USER_SUCCESS = 'users/update_account_user_success',
|
||||
DELETE_ACCOUNT_USER_SUCCESS = 'users/delete_account_user_success',
|
||||
|
||||
CHANGE_USER_ROLES_START = 'users/change_user_roles_start',
|
||||
CHANGE_USER_ROLES_FAILURE = 'users/change_user_roles_failure',
|
||||
FETCH_USER_ROLES_SUCCESS = 'users/fetch_user_roles_success',
|
||||
ADD_USER_ROLE_SUCCESS = 'users/add_user_role_success',
|
||||
UPDATE_USER_ROLE_SUCCESS = 'users/update_user_role_success',
|
||||
DELETE_USER_ROLE_SUCCESS = 'users/delete_user_role_success',
|
||||
}
|
||||
|
||||
export const fetchCurrentUser = () =>
|
||||
createApiAction(
|
||||
{
|
||||
path: '/users/me',
|
||||
method: 'GET',
|
||||
},
|
||||
[
|
||||
UserActionTypes.CHANGE_CURRENT_USER_START,
|
||||
UserActionTypes.FETCH_CURRENT_USER_SUCCESS,
|
||||
UserActionTypes.CHANGE_CURRENT_USER_FAILURE,
|
||||
],
|
||||
);
|
||||
|
||||
export const updateCurrentUser = (data: CurrentUserUpdateAPI) =>
|
||||
createApiAction(
|
||||
{
|
||||
path: '/users/me',
|
||||
method: 'PATCH',
|
||||
body: data,
|
||||
},
|
||||
[
|
||||
UserActionTypes.CHANGE_CURRENT_USER_START,
|
||||
UserActionTypes.UPDATE_CURRENT_USER_SUCCESS,
|
||||
UserActionTypes.CHANGE_CURRENT_USER_FAILURE,
|
||||
],
|
||||
);
|
||||
|
||||
export const [fetchUsers, addUser, updateUser, deleteUser] = createCrudApiActions<User>(
|
||||
'/users',
|
||||
UserActionTypes.CHANGE_USERS_START,
|
||||
UserActionTypes.CHANGE_USERS_FAILURE,
|
||||
UserActionTypes.FETCH_USERS_SUCCESS,
|
||||
UserActionTypes.ADD_ACCOUNT_USER_SUCCESS,
|
||||
UserActionTypes.UPDATE_ACCOUNT_USER_SUCCESS,
|
||||
UserActionTypes.DELETE_ACCOUNT_USER_SUCCESS,
|
||||
transformUserForApi,
|
||||
);
|
||||
4
src/services/users/redux/index.ts
Normal file
4
src/services/users/redux/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export * from './actions';
|
||||
export { default as reducer } from './reducers';
|
||||
export { getCurrentUser, getUsers } from './selectors';
|
||||
export * from './types';
|
||||
61
src/services/users/redux/reducers.ts
Normal file
61
src/services/users/redux/reducers.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { combineReducers } from 'redux';
|
||||
|
||||
import { createApiReducer, chainReducers, INITIAL_API_STATE, createCrudApiReducer } from 'src/services/api';
|
||||
import { AuthActionTypes } from 'src/services/auth/redux/actions';
|
||||
|
||||
import { transformUser } from '../transformations';
|
||||
import { User } from '../types';
|
||||
import { UserActionTypes } from './actions';
|
||||
|
||||
const signInReducer = createApiReducer(
|
||||
[AuthActionTypes.SIGN_IN_START, AuthActionTypes.SIGN_IN_SUCCESS, AuthActionTypes.SIGN_IN_FAILURE],
|
||||
transformUser,
|
||||
(data) => data,
|
||||
);
|
||||
|
||||
const fetchCurrentUserReducer = createApiReducer(
|
||||
[
|
||||
UserActionTypes.CHANGE_CURRENT_USER_START,
|
||||
UserActionTypes.FETCH_CURRENT_USER_SUCCESS,
|
||||
UserActionTypes.CHANGE_CURRENT_USER_FAILURE,
|
||||
],
|
||||
transformUser,
|
||||
(data) => data,
|
||||
);
|
||||
|
||||
const updateCurrentUserReducer = createApiReducer(
|
||||
[
|
||||
UserActionTypes.CHANGE_CURRENT_USER_START,
|
||||
UserActionTypes.UPDATE_CURRENT_USER_SUCCESS,
|
||||
UserActionTypes.CHANGE_CURRENT_USER_FAILURE,
|
||||
],
|
||||
transformUser,
|
||||
(data) => data,
|
||||
);
|
||||
|
||||
const signOutReducer = createApiReducer(
|
||||
['', AuthActionTypes.SIGN_OUT, ''],
|
||||
() => INITIAL_API_STATE,
|
||||
() => INITIAL_API_STATE,
|
||||
);
|
||||
|
||||
const usersReducer = createCrudApiReducer<User>(
|
||||
UserActionTypes.CHANGE_USERS_START,
|
||||
UserActionTypes.CHANGE_USERS_FAILURE,
|
||||
UserActionTypes.FETCH_USERS_SUCCESS,
|
||||
UserActionTypes.ADD_ACCOUNT_USER_SUCCESS,
|
||||
UserActionTypes.UPDATE_ACCOUNT_USER_SUCCESS,
|
||||
UserActionTypes.DELETE_ACCOUNT_USER_SUCCESS,
|
||||
transformUser,
|
||||
);
|
||||
|
||||
export default combineReducers({
|
||||
currentUser: chainReducers(
|
||||
INITIAL_API_STATE,
|
||||
signInReducer,
|
||||
fetchCurrentUserReducer,
|
||||
updateCurrentUserReducer,
|
||||
signOutReducer,
|
||||
),
|
||||
users: usersReducer,
|
||||
});
|
||||
4
src/services/users/redux/selectors.ts
Normal file
4
src/services/users/redux/selectors.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { State } from 'src/redux';
|
||||
|
||||
export const getCurrentUser = (state: State) => state.users.currentUser;
|
||||
export const getUsers = (state: State) => state.users.users;
|
||||
22
src/services/users/redux/types.ts
Normal file
22
src/services/users/redux/types.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { ApiState, ApiStatus } from 'src/services/api/redux';
|
||||
|
||||
import { User } from '../types';
|
||||
|
||||
export interface CurrentUserState extends User {
|
||||
_status: ApiStatus;
|
||||
}
|
||||
|
||||
export interface UsersState {
|
||||
currentUser: CurrentUserState;
|
||||
users: ApiState<User>;
|
||||
}
|
||||
|
||||
export interface CurrentUserUpdateAPI {
|
||||
phoneNumber?: string;
|
||||
email?: string;
|
||||
language?: string;
|
||||
country?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
password?: string;
|
||||
}
|
||||
23
src/services/users/transformations.ts
Normal file
23
src/services/users/transformations.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import _ from 'lodash';
|
||||
|
||||
import { FormUser, User, UserApiRequest } from './types';
|
||||
|
||||
export const transformUserForApi = (user: FormUser): UserApiRequest => ({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
status: user.status,
|
||||
last_login: user.last_login,
|
||||
});
|
||||
|
||||
export const transformUser = (response: any): User => {
|
||||
const userResponse = _.get(response, 'user', response);
|
||||
|
||||
return {
|
||||
id: userResponse.id,
|
||||
email: userResponse.email,
|
||||
name: userResponse.name,
|
||||
last_login: userResponse.last_login,
|
||||
status: userResponse.status,
|
||||
};
|
||||
};
|
||||
26
src/services/users/types.ts
Normal file
26
src/services/users/types.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
export interface User {
|
||||
id: number | null;
|
||||
email: string;
|
||||
name: string;
|
||||
last_login: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface FormUser extends User {
|
||||
password?: string;
|
||||
confirmPassword?: string;
|
||||
}
|
||||
|
||||
export interface UserRole {
|
||||
id?: number;
|
||||
name: string;
|
||||
isAdministrator: boolean;
|
||||
}
|
||||
|
||||
export interface UserApiRequest {
|
||||
id: number | null;
|
||||
email: string;
|
||||
name: string;
|
||||
last_login: string;
|
||||
status: string;
|
||||
}
|
||||
Reference in a new issue