Refactor backend API calls and add toasts

This commit is contained in:
IRBorisov 2023-07-15 17:57:25 +03:00
parent eb045905d8
commit 484b1a9ea1
16 changed files with 343 additions and 231 deletions

View File

@ -12,12 +12,19 @@ import RegisterPage from './pages/RegisterPage';
import ManualsPage from './pages/ManualsPage';
import Footer from './components/Footer';
import RSFormCreatePage from './pages/RSFormCreatePage';
import ToasterThemed from './components/ToasterThemed';
function App() {
function App() {
return (
<div className='antialiased bg-gray-50 dark:bg-gray-800'>
<Navigation />
<main className='min-h-[calc(100vh-107px)] px-2 h-fit'>
<ToasterThemed
className='mt-[4rem]'
autoClose={3000}
draggable={false}
limit={5}
/>
<Routes>
<Route path='/' element={ <HomePage/>} />

View File

@ -0,0 +1,179 @@
import axios, { AxiosResponse } from 'axios'
import { config } from './constants'
import { ErrorInfo } from './components/BackendError'
import { toast } from 'react-toastify'
import { ICurrentUser, IRSForm, IUserInfo, IUserProfile } from './models'
export type BackendCallback = (response: AxiosResponse) => void;
export interface IFrontRequest {
onSucccess?: BackendCallback
onError?: (error: ErrorInfo) => void
setLoading?: (loading: boolean) => void
showError?: boolean
data?: any
}
interface IAxiosRequest {
endpoint: string
request?: IFrontRequest
title?: string
}
// ================= Export API ==============
export async function postLogin(request?: IFrontRequest) {
AxiosPost({
title: 'Login',
endpoint: `${config.url.AUTH}login`,
request: request
});
}
export async function getAuth(request?: IFrontRequest) {
AxiosGet<ICurrentUser>({
title: 'Current user',
endpoint: `${config.url.AUTH}auth`,
request: request
});
}
export async function getProfile(request?: IFrontRequest) {
AxiosGet<IUserProfile>({
title: 'Current user profile',
endpoint: `${config.url.AUTH}profile`,
request: request
});
}
export async function postLogout(request?: IFrontRequest) {
AxiosPost({
title: 'Logout',
endpoint: `${config.url.AUTH}logout`,
request: request
});
};
export async function postSignup(request?: IFrontRequest) {
AxiosPost({
title: 'Register user',
endpoint: `${config.url.AUTH}signup`,
request: request
});
}
export async function getActiveUsers(request?: IFrontRequest) {
AxiosGet<IUserInfo>({
title: 'Active users list',
endpoint: `${config.url.AUTH}active-users`,
request: request
});
}
export async function getRSForms(request?: IFrontRequest) {
AxiosGet<IRSForm[]>({
title: `RSForms list`,
endpoint: `${config.url.BASE}rsforms/`,
request: request
});
}
export async function postNewRSForm(request?: IFrontRequest) {
AxiosPost({
title: `New RSForm`,
endpoint: `${config.url.BASE}rsforms/create-detailed/`,
request: request
});
}
export async function getRSFormDetails(target: string, request?: IFrontRequest) {
AxiosGet<IRSForm>({
title: `RSForm details for id=${target}`,
endpoint: `${config.url.BASE}rsforms/${target}/details/`,
request: request
});
}
export async function patchRSForm(target: string, request?: IFrontRequest) {
AxiosPatch({
title: `RSForm id=${target}`,
endpoint: `${config.url.BASE}rsforms/${target}/`,
request: request
});
}
export async function deleteRSForm(target: string, request?: IFrontRequest) {
AxiosDelete({
title: `RSForm id=${target}`,
endpoint: `${config.url.BASE}rsforms/${target}/`,
request: request
});
}
export async function postClaimRSForm(target: string, request?: IFrontRequest) {
AxiosPost({
title: `Claim on RSForm id=${target}`,
endpoint: `${config.url.BASE}rsforms/${target}/claim/`,
request: request
});
}
// ====== Helper functions ===========
function AxiosGet<ReturnType>({endpoint, request, title}: IAxiosRequest) {
if (title) console.log(`[[${title}]] requested`);
if (request?.setLoading) request?.setLoading(true);
axios.get<ReturnType>(endpoint)
.then(function (response) {
if (request?.setLoading) request?.setLoading(false);
if (request?.onSucccess) request.onSucccess(response);
})
.catch(function (error) {
if (request?.setLoading) request?.setLoading(false);
if (request?.showError) toast.error(error.message);
if (request?.onError) request.onError(error);
});
}
function AxiosPost({endpoint, request, title}: IAxiosRequest) {
if (title) console.log(`[[${title}]] posted`);
if (request?.setLoading) request?.setLoading(true);
axios.post(endpoint, request?.data)
.then(function (response) {
if (request?.setLoading) request?.setLoading(false);
if (request?.onSucccess) request.onSucccess(response);
})
.catch(function (error) {
if (request?.setLoading) request?.setLoading(false);
if (request?.showError) toast.error(error.message);
if (request?.onError) request.onError(error);
});
}
function AxiosDelete({endpoint, request, title}: IAxiosRequest) {
if (title) console.log(`[[${title}]] is being deleted`);
if (request?.setLoading) request?.setLoading(true);
axios.delete(endpoint)
.then(function (response) {
if (request?.setLoading) request?.setLoading(false);
if (request?.onSucccess) request.onSucccess(response);
})
.catch(function (error) {
if (request?.setLoading) request?.setLoading(false);
if (request?.showError) toast.error(error.message);
if (request?.onError) request.onError(error);
});
}
function AxiosPatch({endpoint, request, title}: IAxiosRequest) {
if (title) console.log(`[[${title}]] is being patrially updated`);
if (request?.setLoading) request?.setLoading(true);
axios.patch(endpoint, request?.data)
.then(function (response) {
if (request?.setLoading) request?.setLoading(false);
if (request?.onSucccess) request.onSucccess(response);
})
.catch(function (error) {
if (request?.setLoading) request?.setLoading(false);
if (request?.showError) toast.error(error.message);
if (request?.onError) request.onError(error);
});
}

View File

@ -0,0 +1,14 @@
import { ToastContainer, ToastContainerProps } from 'react-toastify';
import { useTheme } from '../context/ThemeContext';
function ToasterThemed({theme, ...props}: ToastContainerProps) {
const { darkMode } = useTheme();
return (
<ToastContainer
theme={ darkMode ? 'dark' : 'light'}
{...props}
/>
);
}
export default ToasterThemed;

View File

@ -1,17 +1,15 @@
import axios from 'axios';
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
import { config } from '../constants';
import { ICurrentUser, IUserSignupData } from '../models';
import { ErrorInfo } from '../components/BackendError';
import useLocalStorage from '../hooks/useLocalStorage';
import { getAuth, postLogin, postLogout, postSignup } from '../backendAPI';
interface IAuthContext {
user: ICurrentUser | undefined
login: (username: string, password: string, onSuccess?: Function) => void
logout: (onSuccess?: Function) => void
signup: (data: IUserSignupData, onSuccess?: Function) => void
login: (username: string, password: string, onSuccess?: () => void) => void
logout: (onSuccess?: () => void) => void
signup: (data: IUserSignupData, onSuccess?: () => void) => void
loading: boolean
error: ErrorInfo
setError: (error: ErrorInfo) => void
@ -38,74 +36,55 @@ export const AuthState = ({ children }: AuthStateProps) => {
const loadCurrentUser = useCallback(
async () => {
setError(undefined);
setLoading(true);
console.log('Current user requested');
axios.get<ICurrentUser>(`${config.url.AUTH}auth`)
.then(function (response) {
setLoading(false);
if (response.data.id) {
setUser(response.data);
} else {
setUser(undefined)
getAuth({
onError: error => setUser(undefined),
onSucccess: response => {
if (response.data.id) {
setUser(response.data);
} else {
setUser(undefined)
}
}
})
.catch(function (error) {
setLoading(false);
setUser(undefined);
setError(error);
});
}, [setUser]
);
async function login(uname: string, pw: string, onSuccess?: Function) {
setLoading(true);
async function login(uname: string, pw: string, onSuccess?: () => void) {
setError(undefined);
axios.post(`${config.url.AUTH}login`, {username: uname, password: pw})
.then(function (response) {
setLoading(false);
loadCurrentUser();
if(onSuccess) {
onSuccess();
postLogin({
data: {username: uname, password: pw},
showError: true,
setLoading: setLoading,
onError: error => setError(error),
onSucccess: response => {
loadCurrentUser();
if(onSuccess) onSuccess();
}
})
.catch(function (error) {
setLoading(false);
setError(error);
});
}
async function logout(onSuccess?: Function) {
setLoading(true);
async function logout(onSuccess?: () => void) {
setError(undefined);
axios.post(`${config.url.AUTH}logout`)
.then(function (response) {
setLoading(false);
loadCurrentUser();
if(onSuccess) {
onSuccess();
postLogout({
showError: true,
onSucccess: response => {
loadCurrentUser();
if(onSuccess) onSuccess();
}
})
.catch(function (error) {
setLoading(false);
setError(error);
});
}
async function signup(data: IUserSignupData, onSuccess?: Function) {
setLoading(true);
async function signup(data: IUserSignupData, onSuccess?: () => void) {
setError(undefined);
axios.post(`${config.url.AUTH}signup`, data)
.then(function (response) {
setLoading(false);
loadCurrentUser();
if(onSuccess) {
onSuccess();
postSignup({
data: data,
showError: true,
setLoading: setLoading,
onError: error => setError(error),
onSucccess: response => {
loadCurrentUser();
if(onSuccess) onSuccess();
}
})
.catch(function (error) {
setLoading(false);
setError(error);
});
}

View File

@ -3,8 +3,7 @@ import { IConstituenta, IRSForm } from '../models';
import { useRSFormDetails } from '../hooks/useRSFormDetails';
import { ErrorInfo } from '../components/BackendError';
import { useAuth } from './AuthContext';
import axios from 'axios';
import { config } from '../constants';
import { BackendCallback, deleteRSForm, patchRSForm, postClaimRSForm } from '../backendAPI';
interface IRSFormContext {
schema?: IRSForm
@ -17,9 +16,9 @@ interface IRSFormContext {
setActive: (cst: IConstituenta) => void
reload: () => void
upload: (data: any, callback: Function) => void
destroy: (callback: Function) => void
claim: (callback: Function) => void
upload: (data: any, callback?: BackendCallback) => void
destroy: (callback: BackendCallback) => void
claim: (callback: BackendCallback) => void
}
export const RSFormContext = createContext<IRSFormContext>({
@ -58,49 +57,34 @@ export const RSFormState = ({ id, children }: RSFormStateProps) => {
}
}, [schema])
async function upload(data: any, callback?: Function) {
console.log(`Update rsform with ${data}`);
data['id'] = {id}
async function upload(data: any, callback?: BackendCallback) {
setError(undefined);
setProcessing(true);
axios.patch(`${config.url.BASE}rsforms/${id}/`, data)
.then(function (response) {
setProcessing(false);
if (callback) callback(response.data);
})
.catch(function (error) {
setProcessing(false);
setError(error);
patchRSForm(id, {
data: data,
showError: true,
setLoading: setProcessing,
onError: error => setError(error),
onSucccess: callback
});
}
async function destroy(callback?: Function) {
console.log(`Deleting rsform ${id}`);
async function destroy(callback: BackendCallback) {
setError(undefined);
setProcessing(true);
axios.delete(`${config.url.BASE}rsforms/${id}/`)
.then(function (response) {
setProcessing(false);
if (callback) callback();
})
.catch(function (error) {
setProcessing(false);
setError(error);
deleteRSForm(id, {
showError: true,
setLoading: setProcessing,
onError: error => setError(error),
onSucccess: callback
});
}
async function claim(callback?: Function) {
console.log(`Claiming rsform ${id}`);
async function claim(callback: BackendCallback) {
setError(undefined);
setProcessing(true);
axios.post(`${config.url.BASE}rsforms/${id}/claim/`)
.then(function (response) {
setProcessing(false);
if (callback) callback();
})
.catch(function (error) {
setProcessing(false);
setError(error);
postClaimRSForm(id, {
showError: true,
setLoading: setProcessing,
onError: error => setError(error),
onSucccess: callback
});
}

View File

@ -1,22 +1,16 @@
import axios from 'axios'
import { createContext, useCallback, useContext, useEffect, useState } from 'react'
import { IUserInfo } from '../models'
import { config } from '../constants'
import { ErrorInfo } from '../components/BackendError'
import { getActiveUsers } from '../backendAPI'
interface IUsersContext {
users: IUserInfo[]
error: ErrorInfo
loading: boolean
reload: () => void
getUserLabel: (userID?: number) => string
}
export const UsersContext = createContext<IUsersContext>({
users: [],
error: undefined,
loading: false,
reload: () => {},
getUserLabel: () => ''
})
@ -27,8 +21,6 @@ interface UsersStateProps {
export const UsersState = ({ children }: UsersStateProps) => {
const [users, setUsers] = useState<IUserInfo[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<ErrorInfo>(undefined);
const getUserLabel = (userID?: number) => {
const user = users.find(({id}) => id === userID);
@ -49,18 +41,12 @@ export const UsersState = ({ children }: UsersStateProps) => {
const reload = useCallback(
async () => {
setError(undefined);
setLoading(true);
console.log('Profile requested');
axios.get<IUserInfo[]>(`${config.url.AUTH}active-users`)
.then(function (response) {
setLoading(false);
setUsers(response.data);
})
.catch(function (error) {
setLoading(false);
setUsers([]);
setError(error);
getActiveUsers({
showError: true,
onError: error => setUsers([]),
onSucccess: response => {
setUsers(response ? response.data : []);
}
});
}, [setUsers]
);
@ -72,7 +58,6 @@ export const UsersState = ({ children }: UsersStateProps) => {
return (
<UsersContext.Provider value={{
users,
error, loading,
reload, getUserLabel
}}>
{ children }

View File

@ -1,29 +0,0 @@
import axios from 'axios'
import { IUserProfile } from '../models'
import { config } from '../constants'
import { ErrorInfo } from '../components/BackendError'
function useAPI() {
async function getProfile(
args?: {onSucccess: Function, onError: Function}) {
// setError(undefined);
// setLoading(true);
// console.log('Profile requested');
// axios.get<IUserProfile>(`${config.url.AUTH}profile`)
// .then(function (response) {
// setLoading(false);
// return response.data;
// })
// .catch(function (error) {
// setLoading(false);
// setError(error);
// return undefined;
// });
}
return {
getProfile
};
}
export default useAPI;

View File

@ -1,33 +1,30 @@
import axios from 'axios'
import { useState } from 'react'
import { config } from '../constants'
import { ErrorInfo } from '../components/BackendError';
import { postNewRSForm } from '../backendAPI';
function useNewRSForm({callback}: {callback: (newID: string) => void}) {
function useNewRSForm() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<ErrorInfo>(undefined);
async function createNew({data, file}: {data: any, file?: File}) {
async function createSchema({data, file, onSuccess}: {
data: any, file?: File,
onSuccess: (newID: string) => void
}) {
setError(undefined);
setLoading(true);
if (file) {
data['file']=file;
data['fileName']=file.name;
data['file'] = file;
data['fileName'] = file.name;
}
axios.post(`${config.url.BASE}rsforms/create-detailed/`, data)
.then(function (response) {
setLoading(false);
if(callback) {
callback(response.data.id);
}
})
.catch(function (error) {
setLoading(false);
setError(error);
postNewRSForm({
data: data,
showError: true,
setLoading: setLoading,
onError: error => setError(error),
onSucccess: response => onSuccess(response.data.id)
});
}
return { createNew, error, setError, loading };
return { createSchema, error, setError, loading };
}
export default useNewRSForm;

View File

@ -1,8 +1,7 @@
import axios from 'axios'
import { config } from '../constants';
import { useCallback, useEffect, useState } from 'react'
import { IRSForm } from '../models'
import { ErrorInfo } from '../components/BackendError';
import { getRSFormDetails } from '../backendAPI';
export function useRSFormDetails({target}: {target?: string}) {
const [schema, setSchema] = useState<IRSForm | undefined>();
@ -10,21 +9,16 @@ export function useRSFormDetails({target}: {target?: string}) {
const [error, setError] = useState<ErrorInfo>(undefined);
const fetchData = useCallback(async () => {
console.log(`Requesting rsform ${target}`);
setError(undefined);
setSchema(undefined);
if (!target) {
return;
}
setLoading(true);
axios.get<IRSForm>(`${config.url.BASE}rsforms/${target}/details/`)
.then(function (response) {
setLoading(false);
setSchema(response.data);
})
.catch(function (error) {
setLoading(false);
setError(error);
getRSFormDetails(target, {
showError: true,
setLoading: setLoading,
onError: error => setError(error),
onSucccess: response => setSchema(response.data)
});
}, [target]);

View File

@ -1,32 +1,25 @@
import axios from 'axios'
import { useEffect, useState } from 'react'
import { useCallback, useEffect, useState } from 'react'
import { IRSForm } from '../models'
import { config } from '../constants'
import { ErrorInfo } from '../components/BackendError';
import { getRSForms } from '../backendAPI';
export function useRSForms() {
const [rsforms, setRSForms] = useState<IRSForm[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<ErrorInfo>(undefined);
async function fetchRSForms() {
setError(undefined);
setLoading(true);
console.log('RSForms requested');
axios.get<IRSForm[]>(`${config.url.BASE}rsforms/`)
.then(function (response) {
setLoading(false);
setRSForms(response.data);
})
.catch(function (error) {
setLoading(false);
setError(error);
const fetchData = useCallback(async () => {
getRSForms({
showError: true,
setLoading: setLoading,
onError: error => setError(error),
onSucccess: response => setRSForms(response.data)
});
}
}, []);
useEffect(() => {
fetchRSForms();
}, [])
fetchData();
}, [fetchData])
return { rsforms, error, loading };
}

View File

@ -1,8 +1,7 @@
import axios from 'axios'
import { useCallback, useEffect, useState } from 'react'
import { IUserProfile } from '../models'
import { config } from '../constants'
import { ErrorInfo } from '../components/BackendError'
import { getProfile } from '../backendAPI'
export function useUserProfile() {
@ -13,17 +12,12 @@ export function useUserProfile() {
const fetchUser = useCallback(
async () => {
setError(undefined);
setLoading(true);
console.log('Profile requested');
axios.get<IUserProfile>(`${config.url.AUTH}profile`)
.then(function (response) {
setLoading(false);
setUser(response.data);
})
.catch(function (error) {
setLoading(false);
setUser(undefined);
setError(error);
setUser(undefined);
getProfile({
showError: true,
setLoading: setLoading,
onError: error => setError(error),
onSucccess: response => setUser(response.data)
});
}, [setUser]
)

View File

@ -2,9 +2,10 @@
import React from 'react';
import axios from 'axios';
import './index.css';
import 'react-toastify/dist/ReactToastify.css';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import './index.css';
import App from './App';
import { AuthState } from './context/AuthContext';
import { ThemeState } from './context/ThemeContext';

View File

@ -20,6 +20,7 @@ function LoginPage() {
useEffect(() => {
const name = new URLSearchParams(search).get('username');
setUsername(name || '');
setPassword('');
}, [search]);
useEffect(() => {
@ -37,7 +38,7 @@ function LoginPage() {
<div className='container py-2'> { user ?
<InfoMessage message={`Вы вошли в систему как ${user.username}`} />
:
<Form title='Ввод данных пользователя' onSubmit={handleSubmit} widthClass='max-w-sm'>
<Form title='Ввод данных пользователя' onSubmit={handleSubmit} widthClass='w-[20rem]'>
<TextInput id='username'
label='Имя пользователя'
required
@ -49,10 +50,11 @@ function LoginPage() {
label='Пароль'
required
type='password'
value={password}
onChange={event => setPassword(event.target.value)}
/>
<div className='flex items-center justify-between mt-4 mb-2'>
<div className='flex items-center justify-between mt-4'>
<SubmitButton text='Вход' loading={loading}/>
<TextURL text='Восстановить пароль...' href='restore-password' />
</div>

View File

@ -11,6 +11,7 @@ import { useNavigate } from 'react-router-dom';
import TextArea from '../components/Common/TextArea';
import Checkbox from '../components/Common/Checkbox';
import FileInput from '../components/Common/FileInput';
import { toast } from 'react-toastify';
function RSFormCreatePage() {
const navigate = useNavigate();
@ -29,8 +30,11 @@ function RSFormCreatePage() {
}
}
const onSuccess = (newID: string) => navigate(`/rsforms/${newID}`);
const { createNew, error, setError, loading } = useNewRSForm({callback: onSuccess})
const onSuccess = (newID: string) => {
toast.success('Схема успешно создана');
navigate(`/rsforms/${newID}`);
}
const { createSchema, error, setError, loading } = useNewRSForm()
useEffect(() => {
setError(undefined)
@ -45,7 +49,11 @@ function RSFormCreatePage() {
'comment': comment,
'is_common': common,
};
createNew({data: data, file: file});
createSchema({
data: data,
file: file,
onSuccess: onSuccess
});
}
};

View File

@ -6,10 +6,10 @@ import TextInput from '../../components/Common/TextInput';
import { useRSForm } from '../../context/RSFormContext';
import { useCallback, useEffect, useState } from 'react';
import Button from '../../components/Common/Button';
import { CrownIcon, DownloadIcon, DumpBinIcon, UploadIcon } from '../../components/Icons';
import { CrownIcon, DownloadIcon, DumpBinIcon } from '../../components/Icons';
import { useUsers } from '../../context/UsersContext';
import { useNavigate } from 'react-router-dom';
import FileInput from '../../components/Common/FileInput';
import { toast } from 'react-toastify';
function RSFormCard() {
const navigate = useNavigate();
@ -21,8 +21,6 @@ function RSFormCard() {
const [alias, setAlias] = useState('');
const [comment, setComment] = useState('');
const [common, setCommon] = useState(false);
const onSuccess = (data: any) => reload();
useEffect(() => {
setTitle(schema!.title)
@ -40,28 +38,34 @@ function RSFormCard() {
'comment': comment,
'is_common': common,
};
upload(data, onSuccess);
upload(data, () => {
toast.success('Изменения сохранены');
reload();
});
}
};
const handleDelete = useCallback(() => {
if (window.confirm('Вы уверены, что хотите удалить данную схему?')) {
destroy(() => navigate('/rsforms?filter=owned'))
destroy(() => {
toast.success('Схема удалена');
navigate('/rsforms?filter=owned');
});
}
}, [destroy, navigate]);
const handleClaimOwner = useCallback(() => {
if (window.confirm('Вы уверены, что хотите стать владельцем данной схемы?')) {
claim(() => reload());
claim(() => {
toast.success('Вы стали владельцем схемы');
reload();
});
}
}, [claim, reload]);
const handleUpload = useCallback(() => {
}, []);
const handleDownload = useCallback(() => {
// TODO: implement file download
toast.info('Загрузка в разработке');
}, []);
return (

View File

@ -40,11 +40,11 @@ function RegisterPage() {
};
return (
<>
<div className='container py-2'>
{ success &&
<div className='flex flex-col items-center'>
<InfoMessage message={`Вы успешно зарегистрировали пользователя ${username}`}/>
<TextURL text='Войти в аккаунт' href={`login?username=${username}`}/>
<TextURL text='Войти в аккаунт' href={`/login?username=${username}`}/>
</div>}
{ !success && user &&
<InfoMessage message={`Вы вошли в систему как ${user.username}. Если хотите зарегистрировать нового пользователя, выйдите из системы (меню в правом верхнем углу экрана)`} /> }
@ -90,7 +90,7 @@ function RegisterPage() {
{ error && <BackendError error={error} />}
</Form>
}
</>
</div>
);
}