Refactor query message toast
This commit is contained in:
1
apps/frontend/app/empty-toastify.css
Normal file
1
apps/frontend/app/empty-toastify.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/* intentionally empty: used to stub react-toastify CSS in production builds */
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
import { Text } from '@radix-ui/themes';
|
import { Text } from '@radix-ui/themes';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import { useLocation, useNavigate } from 'react-router';
|
import { useLocation, useNavigate } from 'react-router';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify/unstyled';
|
||||||
import { SearchParamKeys } from '../lib/constants';
|
import { SearchParamKeys } from '../lib/constants';
|
||||||
|
import { useQueryMessage } from './useQueryMessage';
|
||||||
|
import { QueryMessageCode, QueryMessageType } from '../lib/QueryMessages';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
export enum ResponseErrorToastId {
|
export enum ResponseErrorToastId {
|
||||||
NetworkError = 'network-error',
|
NetworkError = 'network-error',
|
||||||
@@ -19,95 +22,99 @@ export type DefaultResponseErrorHandlerOptions = {
|
|||||||
* @param err error value
|
* @param err error value
|
||||||
* @returns {boolean} true if the error was handled, false otherwise
|
* @returns {boolean} true if the error was handled, false otherwise
|
||||||
*/
|
*/
|
||||||
const defaultResponseErrorHandler =
|
|
||||||
(navigate: ReturnType<typeof useNavigate>, location: ReturnType<typeof useLocation>) =>
|
|
||||||
(err: unknown, options?: DefaultResponseErrorHandlerOptions): boolean => {
|
|
||||||
if (!(err instanceof AxiosError) && !options?.disableHandleUnexpectedErrors) {
|
|
||||||
toast.error(
|
|
||||||
<div>
|
|
||||||
<Text weight="bold">Unexpected Error:</Text>
|
|
||||||
<br /> An unexpected error occurred. Please try again later.
|
|
||||||
</div>,
|
|
||||||
{
|
|
||||||
position: 'top-center',
|
|
||||||
autoClose: false,
|
|
||||||
hideProgressBar: false,
|
|
||||||
closeOnClick: true,
|
|
||||||
pauseOnHover: true,
|
|
||||||
draggable: false,
|
|
||||||
progress: undefined,
|
|
||||||
theme: 'colored',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(err instanceof AxiosError)) return false;
|
|
||||||
|
|
||||||
if (err.message === 'canceled') {
|
|
||||||
// request was aborted, ignore but return true to indicate it was handled
|
|
||||||
return !options?.disableIgnoreCanceledRequests;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (err.message === 'Network Error') {
|
|
||||||
toast.error(
|
|
||||||
<div>
|
|
||||||
<Text weight="bold">Network Error:</Text>
|
|
||||||
<br /> Unable to reach the server. Please check your internet connection and try again.
|
|
||||||
</div>,
|
|
||||||
{
|
|
||||||
toastId: ResponseErrorToastId.NetworkError,
|
|
||||||
position: 'top-center',
|
|
||||||
autoClose: false,
|
|
||||||
hideProgressBar: false,
|
|
||||||
closeOnClick: true,
|
|
||||||
pauseOnHover: true,
|
|
||||||
draggable: false,
|
|
||||||
progress: undefined,
|
|
||||||
theme: 'colored',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle 401 Unauthorized globally
|
|
||||||
if (err.status === 401 && !options?.disableUnauthorizedHandling) {
|
|
||||||
// store current path for redirect after login
|
|
||||||
const currentPath = location.pathname + location.search;
|
|
||||||
const searchParam = new URLSearchParams();
|
|
||||||
searchParam.set(SearchParamKeys.Redirect, currentPath);
|
|
||||||
searchParam.set(SearchParamKeys.Message, 'Session expired, please log in again');
|
|
||||||
navigate(`/login?${searchParam.toString()}`);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (err.status === 403) {
|
|
||||||
toast.error(
|
|
||||||
<div>
|
|
||||||
<Text weight="bold">Forbidden:</Text>
|
|
||||||
<br /> You do not have permission to perform this action.
|
|
||||||
</div>,
|
|
||||||
{
|
|
||||||
position: 'top-center',
|
|
||||||
autoClose: 5000,
|
|
||||||
hideProgressBar: false,
|
|
||||||
closeOnClick: true,
|
|
||||||
pauseOnHover: true,
|
|
||||||
draggable: false,
|
|
||||||
progress: undefined,
|
|
||||||
theme: 'colored',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function useResponseErrorHandler(): {
|
export function useResponseErrorHandler(): {
|
||||||
defaultResponseErrorHandler: ReturnType<typeof defaultResponseErrorHandler>;
|
defaultResponseErrorHandler: typeof defaultResponseErrorHandler;
|
||||||
} {
|
} {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
return { defaultResponseErrorHandler: defaultResponseErrorHandler(navigate, location) };
|
const { toSearchParamQueryMessage } = useQueryMessage();
|
||||||
|
|
||||||
|
const defaultResponseErrorHandler = useCallback(
|
||||||
|
(err: unknown, options?: DefaultResponseErrorHandlerOptions): boolean => {
|
||||||
|
if (!(err instanceof AxiosError) && !options?.disableHandleUnexpectedErrors) {
|
||||||
|
toast.error(
|
||||||
|
<div>
|
||||||
|
<Text weight="bold">Unexpected Error:</Text>
|
||||||
|
<br /> An unexpected error occurred. Please try again later.
|
||||||
|
</div>,
|
||||||
|
{
|
||||||
|
position: 'top-center',
|
||||||
|
autoClose: false,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: false,
|
||||||
|
progress: undefined,
|
||||||
|
theme: 'colored',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(err instanceof AxiosError)) return false;
|
||||||
|
|
||||||
|
if (err.message === 'canceled') {
|
||||||
|
// request was aborted, ignore but return true to indicate it was handled
|
||||||
|
return !options?.disableIgnoreCanceledRequests;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err.message === 'Network Error') {
|
||||||
|
toast.error(
|
||||||
|
<div>
|
||||||
|
<Text weight="bold">Network Error:</Text>
|
||||||
|
<br /> Unable to reach the server. Please check your internet connection and try again.
|
||||||
|
</div>,
|
||||||
|
{
|
||||||
|
toastId: ResponseErrorToastId.NetworkError,
|
||||||
|
position: 'top-center',
|
||||||
|
autoClose: false,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: false,
|
||||||
|
progress: undefined,
|
||||||
|
theme: 'colored',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle 401 Unauthorized globally
|
||||||
|
if (err.status === 401 && !options?.disableUnauthorizedHandling) {
|
||||||
|
// store current path for redirect after login
|
||||||
|
const currentPath = location.pathname + location.search;
|
||||||
|
const searchParam = new URLSearchParams();
|
||||||
|
searchParam.set(SearchParamKeys.Redirect, currentPath);
|
||||||
|
searchParam.set(SearchParamKeys.Message, toSearchParamQueryMessage(QueryMessageCode.SessionExpired, QueryMessageType.Info));
|
||||||
|
navigate(`/login?${searchParam.toString()}`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err.status === 403) {
|
||||||
|
toast.error(
|
||||||
|
<div>
|
||||||
|
<Text weight="bold">Forbidden:</Text>
|
||||||
|
<br /> You do not have permission to perform this action.
|
||||||
|
</div>,
|
||||||
|
{
|
||||||
|
position: 'top-center',
|
||||||
|
autoClose: 5000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: false,
|
||||||
|
progress: undefined,
|
||||||
|
theme: 'colored',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
[location, navigate, toSearchParamQueryMessage]
|
||||||
|
);
|
||||||
|
|
||||||
|
return { defaultResponseErrorHandler };
|
||||||
}
|
}
|
||||||
|
|||||||
111
apps/frontend/app/hooks/useQueryMessage.tsx
Normal file
111
apps/frontend/app/hooks/useQueryMessage.tsx
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import { useCallback, useEffect, useRef, type ReactNode } from 'react';
|
||||||
|
import { useLocation, useSearchParams } from 'react-router';
|
||||||
|
import { toast } from 'react-toastify/unstyled';
|
||||||
|
import { SearchParamKeys } from '../lib/constants';
|
||||||
|
import { CODE_TO_MESSAGE_MAP, QueryMessageCode, QueryMessageType } from '../lib/QueryMessages';
|
||||||
|
|
||||||
|
type QueryMessageString = `${QueryMessageCode}__${QueryMessageType}`;
|
||||||
|
|
||||||
|
export type QueryMessage = {
|
||||||
|
type: QueryMessageType;
|
||||||
|
code: QueryMessageCode;
|
||||||
|
message: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UseQueryMessageOptions = {
|
||||||
|
displayMessages?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UseQueryMessageReturn = {
|
||||||
|
setQueryMessage: (messageCode: QueryMessageCode, messageType: QueryMessageType) => void;
|
||||||
|
clearQueryMessage: () => void;
|
||||||
|
toSearchParamQueryMessage: (message: QueryMessageCode, type: QueryMessageType) => QueryMessageString;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useQueryMessage(
|
||||||
|
{ displayMessages }: UseQueryMessageOptions = {
|
||||||
|
displayMessages: true,
|
||||||
|
}
|
||||||
|
): UseQueryMessageReturn {
|
||||||
|
const location = useLocation();
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
const messageStr = useRef<QueryMessageString | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Reset messageStr when location changes to allow re-displaying the same message on navigation
|
||||||
|
messageStr.current = null;
|
||||||
|
}, [location.pathname]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const queryMessageStr = searchParams.get(SearchParamKeys.Message);
|
||||||
|
if (!(queryMessageStr && queryMessageStr !== messageStr.current)) return;
|
||||||
|
const [queryMessage, queryMessageString] = toQueryMessage(queryMessageStr) ?? [null, null];
|
||||||
|
if (!queryMessage) return;
|
||||||
|
|
||||||
|
messageStr.current = queryMessageString;
|
||||||
|
if (displayMessages) {
|
||||||
|
toast[queryMessage.type](queryMessage.code, {
|
||||||
|
position: 'top-center',
|
||||||
|
autoClose: 5000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: false,
|
||||||
|
progress: undefined,
|
||||||
|
theme: 'colored',
|
||||||
|
toastId: 'login-route-info-message',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [displayMessages, searchParams]);
|
||||||
|
|
||||||
|
const setQueryMessage = useCallback(
|
||||||
|
(messageCode: QueryMessageCode, messageType: QueryMessageType) => {
|
||||||
|
const queryMessageString: QueryMessageString = `${messageCode}__${messageType}`;
|
||||||
|
messageStr.current = queryMessageString;
|
||||||
|
setSearchParams((prev) => {
|
||||||
|
prev.set(SearchParamKeys.Message, queryMessageString);
|
||||||
|
return prev;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[setSearchParams]
|
||||||
|
);
|
||||||
|
|
||||||
|
const clearQueryMessage = useCallback(() => {
|
||||||
|
messageStr.current = null;
|
||||||
|
setSearchParams((prev) => {
|
||||||
|
prev.delete(SearchParamKeys.Message);
|
||||||
|
return prev;
|
||||||
|
});
|
||||||
|
}, [setSearchParams]);
|
||||||
|
|
||||||
|
const toSearchParamQueryMessage = useCallback((message: QueryMessageCode, type: QueryMessageType): QueryMessageString => {
|
||||||
|
return `${message}__${type}`;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
setQueryMessage,
|
||||||
|
clearQueryMessage,
|
||||||
|
toSearchParamQueryMessage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidQueryMessageCode(code: string): code is QueryMessageCode {
|
||||||
|
return Object.values(QueryMessageCode).includes(code as QueryMessageCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidQueryMessageType(type: string): type is QueryMessageType {
|
||||||
|
return Object.values(QueryMessageType).includes(type as QueryMessageType);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toQueryMessage(value: string): [QueryMessage, QueryMessageString] | null {
|
||||||
|
const [code, type] = value.split('__');
|
||||||
|
if (!isValidQueryMessageCode(code) || !isValidQueryMessageType(type)) return null;
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
code: code,
|
||||||
|
type: type,
|
||||||
|
message: CODE_TO_MESSAGE_MAP[code],
|
||||||
|
},
|
||||||
|
`${code}__${type}`,
|
||||||
|
];
|
||||||
|
}
|
||||||
20
apps/frontend/app/lib/QueryMessages.tsx
Normal file
20
apps/frontend/app/lib/QueryMessages.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
export enum QueryMessageType {
|
||||||
|
Info = 'info',
|
||||||
|
Success = 'success',
|
||||||
|
Warning = 'warning',
|
||||||
|
Error = 'error',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum QueryMessageCode {
|
||||||
|
SessionExpired = 'SESSION_EXPIRED',
|
||||||
|
InitializationRequired = 'INITIALIZATION_REQUIRED',
|
||||||
|
InitializationSuccessful = 'INITIALIZATION_SUCCESSFUL',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CODE_TO_MESSAGE_MAP: Record<QueryMessageCode, ReactNode> = {
|
||||||
|
[QueryMessageCode.SessionExpired]: 'Your session has expired. Please log in again.',
|
||||||
|
[QueryMessageCode.InitializationRequired]: 'The application requires initialization. Please follow the setup instructions.',
|
||||||
|
[QueryMessageCode.InitializationSuccessful]: 'Initialization successful. Please log in.',
|
||||||
|
} as const;
|
||||||
@@ -2,6 +2,11 @@ import { isRouteErrorResponse, Links, Meta, Outlet, Scripts, ScrollRestoration }
|
|||||||
import type { Route } from './+types/root';
|
import type { Route } from './+types/root';
|
||||||
import '@radix-ui/themes/styles.css';
|
import '@radix-ui/themes/styles.css';
|
||||||
import './app.css';
|
import './app.css';
|
||||||
|
// start: react-toastify special import
|
||||||
|
// ! MUST use unstyled version for dev server build, styled version for production build is handled in vite.config.ts
|
||||||
|
import { ToastContainer } from 'react-toastify/unstyled';
|
||||||
|
import 'react-toastify/ReactToastify.css';
|
||||||
|
// end: react-toastify special import
|
||||||
import AppTheme from './components/theme';
|
import AppTheme from './components/theme';
|
||||||
import { ApiProvider } from './providers/ApiProvider';
|
import { ApiProvider } from './providers/ApiProvider';
|
||||||
import { LayoutProvider } from './providers/LayoutProvider';
|
import { LayoutProvider } from './providers/LayoutProvider';
|
||||||
@@ -19,8 +24,6 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<Meta />
|
<Meta />
|
||||||
<Links />
|
<Links />
|
||||||
{/* Required for react-toastify */}
|
|
||||||
<style />
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{children}
|
{children}
|
||||||
@@ -33,19 +36,23 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
<AppTheme>
|
<>
|
||||||
<ApiProvider>
|
<AppTheme>
|
||||||
<Tooltip.Provider delayDuration={250}>
|
<ApiProvider>
|
||||||
<LayoutProvider>
|
<Tooltip.Provider delayDuration={250}>
|
||||||
<ApiHealthProvider>
|
<LayoutProvider>
|
||||||
<AuthProvider>
|
<ApiHealthProvider>
|
||||||
<Outlet />
|
<AuthProvider>
|
||||||
</AuthProvider>
|
<Outlet />
|
||||||
</ApiHealthProvider>
|
</AuthProvider>
|
||||||
</LayoutProvider>
|
</ApiHealthProvider>
|
||||||
</Tooltip.Provider>
|
</LayoutProvider>
|
||||||
</ApiProvider>
|
</Tooltip.Provider>
|
||||||
</AppTheme>
|
</ApiProvider>
|
||||||
|
</AppTheme>
|
||||||
|
|
||||||
|
<ToastContainer />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { Box, Container, Flex, Heading } from '@radix-ui/themes';
|
import { Box, Container, Flex, Heading } from '@radix-ui/themes';
|
||||||
import { useMutation } from '@tanstack/react-query';
|
import { useMutation } from '@tanstack/react-query';
|
||||||
import { useLocation, useNavigate } from 'react-router';
|
import { useLocation, useNavigate } from 'react-router';
|
||||||
import { Slide, toast, ToastContainer } from 'react-toastify';
|
import { toast } from 'react-toastify/unstyled';
|
||||||
import * as v from 'valibot';
|
import * as v from 'valibot';
|
||||||
import { useResponseErrorHandler } from '../../hooks/ResponseHelper';
|
import { useResponseErrorHandler } from '../../hooks/ResponseHelper';
|
||||||
import { useApi } from '../../providers/ApiProvider';
|
import { useApi } from '../../providers/ApiProvider';
|
||||||
import { formHook } from '../../providers/FormProvider';
|
import { formHook } from '../../providers/FormProvider';
|
||||||
import type { Route } from './+types/login';
|
import type { Route } from './+types/login';
|
||||||
import { SearchParamKeys } from '../../lib/constants';
|
import { SearchParamKeys } from '../../lib/constants';
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
|
import { useQueryMessage } from '../../hooks/useQueryMessage';
|
||||||
|
|
||||||
const loginFormSchema = v.object({
|
const loginFormSchema = v.object({
|
||||||
username: v.pipe(v.string(), v.trim(), v.minLength(1, 'Username is required')),
|
username: v.pipe(v.string(), v.trim(), v.minLength(1, 'Username is required')),
|
||||||
@@ -27,7 +27,7 @@ export default function LoginRoute() {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { tanstackApiClient } = useApi();
|
const { tanstackApiClient } = useApi();
|
||||||
const { defaultResponseErrorHandler } = useResponseErrorHandler();
|
const { defaultResponseErrorHandler } = useResponseErrorHandler();
|
||||||
const [previousSearchParamMessage, setPreviousSearchParamMessage] = useState<string>('');
|
useQueryMessage();
|
||||||
|
|
||||||
const { mutateAsync: login, isPending } = useMutation({
|
const { mutateAsync: login, isPending } = useMutation({
|
||||||
...tanstackApiClient.mutation('post', '/api/auth/login').mutationOptions,
|
...tanstackApiClient.mutation('post', '/api/auth/login').mutationOptions,
|
||||||
@@ -75,25 +75,6 @@ export default function LoginRoute() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const searchParams = new URLSearchParams(location.search);
|
|
||||||
const message = searchParams.get(SearchParamKeys.Message);
|
|
||||||
if (message && message !== previousSearchParamMessage) {
|
|
||||||
setPreviousSearchParamMessage(message);
|
|
||||||
toast.info(message, {
|
|
||||||
position: 'top-center',
|
|
||||||
autoClose: 5000,
|
|
||||||
hideProgressBar: false,
|
|
||||||
closeOnClick: true,
|
|
||||||
pauseOnHover: true,
|
|
||||||
draggable: false,
|
|
||||||
progress: undefined,
|
|
||||||
theme: 'colored',
|
|
||||||
toastId: 'login-route-info-message',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [location.search]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Flex align="center" justify="center" style={{ minHeight: 'calc(100vh - 64px)' }}>
|
<Flex align="center" justify="center" style={{ minHeight: 'calc(100vh - 64px)' }}>
|
||||||
@@ -167,17 +148,6 @@ export default function LoginRoute() {
|
|||||||
</Box>
|
</Box>
|
||||||
</Container>
|
</Container>
|
||||||
</Flex>
|
</Flex>
|
||||||
<ToastContainer
|
|
||||||
position="top-center"
|
|
||||||
autoClose={false}
|
|
||||||
newestOnTop={false}
|
|
||||||
closeOnClick
|
|
||||||
rtl={false}
|
|
||||||
pauseOnFocusLoss
|
|
||||||
draggable={false}
|
|
||||||
theme="colored"
|
|
||||||
transition={Slide}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { Box, Container, Flex, Heading, Text } from '@radix-ui/themes';
|
import { Box, Container, Flex, Heading, Text } from '@radix-ui/themes';
|
||||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
import { Slide, toast, ToastContainer } from 'react-toastify';
|
import { toast } from 'react-toastify/unstyled';
|
||||||
import * as v from 'valibot';
|
import * as v from 'valibot';
|
||||||
import { useResponseErrorHandler } from '../hooks/ResponseHelper';
|
import { useResponseErrorHandler } from '../hooks/ResponseHelper';
|
||||||
import { useApi } from '../providers/ApiProvider';
|
import { useApi } from '../providers/ApiProvider';
|
||||||
import { formHook } from '../providers/FormProvider';
|
import { formHook } from '../providers/FormProvider';
|
||||||
import { TooltipContentContainer } from '../components/info';
|
import { TooltipContentContainer } from '../components/info';
|
||||||
import { SearchParamKeys } from '../lib/constants';
|
import { SearchParamKeys } from '../lib/constants';
|
||||||
|
import { useQueryMessage } from '../hooks/useQueryMessage';
|
||||||
|
import { QueryMessageCode, QueryMessageType } from '../lib/QueryMessages';
|
||||||
|
|
||||||
const initFormSchema = v.object({
|
const initFormSchema = v.object({
|
||||||
username: v.pipe(v.string(), v.trim(), v.minLength(1, 'Username is required')),
|
username: v.pipe(v.string(), v.trim(), v.minLength(1, 'Username is required')),
|
||||||
@@ -19,12 +21,13 @@ export default function InitRoute() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { tanstackApiClient } = useApi();
|
const { tanstackApiClient } = useApi();
|
||||||
const { defaultResponseErrorHandler } = useResponseErrorHandler();
|
const { defaultResponseErrorHandler } = useResponseErrorHandler();
|
||||||
|
const { toSearchParamQueryMessage } = useQueryMessage();
|
||||||
|
|
||||||
const { mutateAsync: initAdmin, isPending } = useMutation({
|
const { mutateAsync: initAdmin, isPending } = useMutation({
|
||||||
...tanstackApiClient.mutation('post', '/api/auth/init_admin').mutationOptions,
|
...tanstackApiClient.mutation('post', '/api/auth/init_admin').mutationOptions,
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
const searchParams = new URLSearchParams();
|
const searchParams = new URLSearchParams();
|
||||||
searchParams.set(SearchParamKeys.Message, 'Initialization successful. Please log in.');
|
searchParams.set(SearchParamKeys.Message, toSearchParamQueryMessage(QueryMessageCode.InitializationSuccessful, QueryMessageType.Success));
|
||||||
navigate(`/login?${searchParams.toString()}`);
|
navigate(`/login?${searchParams.toString()}`);
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
@@ -40,7 +43,7 @@ export default function InitRoute() {
|
|||||||
try {
|
try {
|
||||||
const data = await healthInfoQuery.queryFn!(...args);
|
const data = await healthInfoQuery.queryFn!(...args);
|
||||||
if (data.is_initialized) {
|
if (data.is_initialized) {
|
||||||
navigate('/');
|
navigate('/login', { replace: true });
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
@@ -153,18 +156,6 @@ export default function InitRoute() {
|
|||||||
</Box>
|
</Box>
|
||||||
</Container>
|
</Container>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<ToastContainer
|
|
||||||
position="top-center"
|
|
||||||
autoClose={false}
|
|
||||||
newestOnTop={false}
|
|
||||||
closeOnClick
|
|
||||||
rtl={false}
|
|
||||||
pauseOnFocusLoss
|
|
||||||
draggable={false}
|
|
||||||
theme="colored"
|
|
||||||
transition={Slide}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,34 @@ import tsconfigPaths from 'vite-tsconfig-paths';
|
|||||||
// @ts-expect-error vite-plugin-eslint has no types
|
// @ts-expect-error vite-plugin-eslint has no types
|
||||||
import eslint from 'vite-plugin-eslint';
|
import eslint from 'vite-plugin-eslint';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig(({ command }) => {
|
||||||
plugins: [
|
const isBuild = command === 'build';
|
||||||
tailwindcss(),
|
|
||||||
reactRouter(),
|
return {
|
||||||
tsconfigPaths(),
|
plugins: [
|
||||||
eslint({
|
tailwindcss(),
|
||||||
failOnError: false,
|
reactRouter(),
|
||||||
}),
|
tsconfigPaths(),
|
||||||
],
|
eslint({
|
||||||
appType: 'spa',
|
failOnError: false,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: isBuild
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
// replace unstyled import with styled for SPA build
|
||||||
|
find: 'react-toastify/unstyled',
|
||||||
|
replacement: 'react-toastify',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// point to the empty CSS file to stub out the import during build, SPA build does not require extra CSS imports
|
||||||
|
find: 'react-toastify/ReactToastify.css',
|
||||||
|
replacement: '~/empty-toastify.css',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
},
|
||||||
|
appType: 'spa',
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user