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}
+}