diff --git a/src/components/Alert/AlertBox.tsx b/src/components/Alert/AlertBox.tsx new file mode 100644 index 00000000..fa617aff --- /dev/null +++ b/src/components/Alert/AlertBox.tsx @@ -0,0 +1,35 @@ +import {Stack} from '@mui/material' +import {FC} from 'react' +import {useContainer} from 'unstated-next' + +import {AlertContainer} from '@/utils/AlertContainer' + +import {Button} from '../Clickable/Button' +import {Dialog} from '../Dialog/Dialog' + +export const AlertBox: FC = () => { + const container = useContainer(AlertContainer) + + const closeContainer = () => { + container.setAlertBox({ + message: container.alertBox?.message ?? '', + title: container.alertBox?.title ?? '', + isOpen: false, + }) + } + + return ( + + + + + + ) +} diff --git a/src/components/PageLayout/LoginForm/LoginForm.tsx b/src/components/PageLayout/LoginForm/LoginForm.tsx index 50973b7d..41366b72 100644 --- a/src/components/PageLayout/LoginForm/LoginForm.tsx +++ b/src/components/PageLayout/LoginForm/LoginForm.tsx @@ -8,6 +8,7 @@ import {Link} from '@/components/Clickable/Link' import {Dialog} from '@/components/Dialog/Dialog' import {FormInput} from '@/components/FormItems/FormInput/FormInput' import {AuthContainer} from '@/utils/AuthContainer' +import {useAlert} from '@/utils/useAlert' import {useSeminarInfo} from '@/utils/useSeminarInfo' import {PasswordResetRequestForm} from '../PasswordResetRequest/PasswordResetRequest' @@ -30,6 +31,7 @@ export const LoginForm: FC = ({closeDialog}) => { const {login} = AuthContainer.useContainer() const {handleSubmit, control} = useForm({defaultValues}) const {seminar} = useSeminarInfo() + const {alert} = useAlert() const router = useRouter() diff --git a/src/components/Problems/UploadProblemForm.tsx b/src/components/Problems/UploadProblemForm.tsx index 120e0abf..ea9fc343 100644 --- a/src/components/Problems/UploadProblemForm.tsx +++ b/src/components/Problems/UploadProblemForm.tsx @@ -6,6 +6,7 @@ import {useDropzone} from 'react-dropzone' import {CloseButton} from '@/components/CloseButton/CloseButton' import {niceBytes} from '@/utils/niceBytes' +import {useAlert} from '@/utils/useAlert' import {Button} from '../Clickable/Button' import {Link} from '../Clickable/Link' @@ -28,6 +29,8 @@ export const UploadProblemForm: FC<{ isAfterDeadline, invalidateSeriesQuery, }) => { + const {alert} = useAlert() + const {mutate: uploadSolution} = useMutation({ mutationFn: (formData: FormData) => axios.post(`/api/competition/problem/${problemId}/upload-solution`, formData), onSuccess: (response) => { diff --git a/src/components/Profile/PasswordChangeForm.tsx b/src/components/Profile/PasswordChangeForm.tsx index c7a2e910..f1994485 100644 --- a/src/components/Profile/PasswordChangeForm.tsx +++ b/src/components/Profile/PasswordChangeForm.tsx @@ -5,6 +5,7 @@ import {FC} from 'react' import {SubmitHandler, useForm} from 'react-hook-form' import {IGeneralPostResponse} from '@/types/api/general' +import {useAlert} from '@/utils/useAlert' import {Button} from '../Clickable/Button' import {Dialog} from '../Dialog/Dialog' @@ -33,6 +34,7 @@ interface ChangePasswordErrorResponseData { export const PasswordChangeDialog: FC = ({open, close}) => { const {handleSubmit, reset, control, getValues, setError} = useForm({defaultValues}) + const {alert} = useAlert() const onSuccess = () => { alert('Zmena hesla prebehla úspešne.') diff --git a/src/components/RegisterForm/RegisterForm.tsx b/src/components/RegisterForm/RegisterForm.tsx index 0c04e537..8a9faedb 100644 --- a/src/components/RegisterForm/RegisterForm.tsx +++ b/src/components/RegisterForm/RegisterForm.tsx @@ -7,6 +7,7 @@ import {SubmitHandler, useForm, useFormState} from 'react-hook-form' import {FormInput} from '@/components/FormItems/FormInput/FormInput' import {IGeneralPostResponse} from '@/types/api/general' +import {useAlert} from '@/utils/useAlert' import {useSeminarInfo} from '@/utils/useSeminarInfo' import {useNavigationTrap} from '../../utils/useNavigationTrap' @@ -62,6 +63,7 @@ export const RegisterForm: FC = () => { const router = useRouter() const {seminar} = useSeminarInfo() + const {alert} = useAlert() const transformFormData = (data: RegisterFormValues) => ({ email: data.email, diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 3896747f..2f94de6e 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -6,11 +6,14 @@ import {isAxiosError} from 'axios' import {AppProps} from 'next/app' import dynamic from 'next/dynamic' import Head from 'next/head' -import {FC} from 'react' +import {FC, PropsWithChildren, useMemo} from 'react' import {CookiesProvider} from 'react-cookie' +import {AlertBox} from '@/components/Alert/AlertBox' import {theme} from '@/theme' +import {AlertContainer} from '@/utils/AlertContainer' import {AuthContainer} from '@/utils/AuthContainer' +import {useAlert} from '@/utils/useAlert' const ReactQueryDevtools = dynamic( () => import('@tanstack/react-query-devtools').then(({ReactQueryDevtools}) => ReactQueryDevtools), @@ -71,6 +74,68 @@ const queryClient = new QueryClient({ }, }) +const ReactQueryProvider: FC = ({children}) => { + const {alert} = useAlert() + + const queryClient = useMemo( + () => + new QueryClient({ + defaultOptions: { + queries: { + retry: (failureCount, error) => { + if (isAxiosError(error)) { + // nechceme retryovat 403 (Forbidden) + const status = error.response?.status + if (status === 403 || status === 404) return false + } + // klasika - retryuj len 3x + if (failureCount >= 3) return false + return true + }, + }, + mutations: { + // globalny error handler requestov cez useMutation + // notes: + // - useMutation vzdy sam loguje error do konzoly, my nemusime + // - specifikovanim `onError` na nejakej `useMutation` sa tento handler prepise, tak sa tomu vyhybajme + onError: (error) => { + if (isAxiosError(error)) { + const data = error.response?.data as unknown + if (typeof data === 'object' && data) { + // ak mame vlastny message v `.detail`, ukazeme userovi ten + const detail = 'detail' in data && data.detail + if (typeof detail === 'string') { + alert(detail) + return + } + + // ak nie, ale mame message v `.non_field_errors`, ukazeme ten + const nonFieldErrors = 'non_field_errors' in data && data.non_field_errors + const nonFieldError = Array.isArray(nonFieldErrors) && (nonFieldErrors as unknown[])[0] + if (typeof nonFieldError === 'string') { + alert(nonFieldError) + return + } + + // TODO: handle field errors (napr. na password) - nealertovat usera, ale zobrazit v UI? alebo alert s prvym field errorom + + // ak nie, ukazeme kludne original anglicku hlasku z `error`u + alert(error.message) + return + } + } else { + alert(error.message) + } + }, + }, + }, + }), + [alert], + ) + + return {children} +} + const MyApp: FC = ({Component, pageProps}) => { return ( <> @@ -86,16 +151,19 @@ const MyApp: FC = ({Component, pageProps}) => { font-family: ${theme.typography.fontFamily}; } `} - - - - - - - - - - + + + + + + + + + + + + + ) } diff --git a/src/utils/AlertContainer.tsx b/src/utils/AlertContainer.tsx new file mode 100644 index 00000000..be86fd59 --- /dev/null +++ b/src/utils/AlertContainer.tsx @@ -0,0 +1,15 @@ +import {useState} from 'react' +import {createContainer} from 'unstated-next' + +export interface AlertProps { + isOpen: boolean + title?: string + message?: string +} + +const useAlertBox = () => { + const [alertBox, setAlertBox] = useState() + return {alertBox, setAlertBox} +} + +export const AlertContainer = createContainer(useAlertBox) diff --git a/src/utils/useAlert.ts b/src/utils/useAlert.ts new file mode 100644 index 00000000..ffa9fa12 --- /dev/null +++ b/src/utils/useAlert.ts @@ -0,0 +1,13 @@ +import {useContainer} from 'unstated-next' + +import {AlertContainer} from '@/utils/AlertContainer' + +export const useAlert = () => { + const container = useContainer(AlertContainer) + + const alert = (message: string, title?: string) => { + container.setAlertBox({message: message, title: title ?? 'Upozornenie', isOpen: true}) + } + + return {alert} +}