ErrorComponent, 404Component, Theming
This commit is contained in:
parent
e6fcd54e22
commit
bf2f4954ee
5
exam/dist/assets/index-BaG4uuqL.js
vendored
5
exam/dist/assets/index-BaG4uuqL.js
vendored
File diff suppressed because one or more lines are too long
5
exam/dist/assets/index-C9ueyNrZ.js
vendored
Normal file
5
exam/dist/assets/index-C9ueyNrZ.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
6
exam/dist/index.html
vendored
6
exam/dist/index.html
vendored
@ -5,10 +5,10 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/phpCourse/exam/dist/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
<script type="module" crossorigin src="/phpCourse/exam/dist/assets/index-BaG4uuqL.js"></script>
|
||||
<script type="module" crossorigin src="/phpCourse/exam/dist/assets/index-C9ueyNrZ.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/react-C_FdcE2X.js">
|
||||
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/mui-aBip8mmu.js">
|
||||
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/tanstack-DojtBDN6.js">
|
||||
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/mui-4z7lf7t1.js">
|
||||
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/tanstack-Do102PZ-.js">
|
||||
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/zustand-DAXCIHlT.js">
|
||||
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/i18n-W-kxdzA-.js">
|
||||
<link rel="stylesheet" crossorigin href="/phpCourse/exam/dist/assets/mui-CKDNpdid.css">
|
||||
|
||||
11
exam/dist/locales/de/translation.json
vendored
11
exam/dist/locales/de/translation.json
vendored
@ -86,5 +86,14 @@
|
||||
|
||||
"Dark": "Dunkel",
|
||||
"Light": "Hell",
|
||||
"System": "System"
|
||||
"System": "System",
|
||||
|
||||
"Confirm success header": "Benutzer aktiviert!",
|
||||
"Confirm error header": "Benutzer existiert nicht oder ist schon aktiviert.",
|
||||
"Confirm pending header": "Benutzer wird aktiviert...",
|
||||
"Back to main": "Zurück zur Hauptseite",
|
||||
|
||||
"Page not found": "Die Seite konnte nicht gefunden werden.",
|
||||
"Session expired": "Deine Sitzung ist abgelaufen.",
|
||||
"General error": "Da ist wohl was schief gelaufen."
|
||||
}
|
||||
|
||||
11
exam/dist/locales/en/translation.json
vendored
11
exam/dist/locales/en/translation.json
vendored
@ -87,5 +87,14 @@
|
||||
|
||||
"Dark": "Dark",
|
||||
"Light": "Light",
|
||||
"System": "System"
|
||||
"System": "System",
|
||||
|
||||
"Confirm success header": "User confirmed!",
|
||||
"Confirm error header": "User does not exist or is already confirmed.",
|
||||
"confirm pending header": "User is getting confirmed...",
|
||||
"Back to main": "Back to the front page",
|
||||
|
||||
"Page not found": "This page was not found",
|
||||
"Session expired": "Your session has expired.",
|
||||
"General error": "Looks like something went wrong."
|
||||
}
|
||||
|
||||
2
exam/dist/stats.html
vendored
2
exam/dist/stats.html
vendored
File diff suppressed because one or more lines are too long
@ -86,5 +86,14 @@
|
||||
|
||||
"Dark": "Dunkel",
|
||||
"Light": "Hell",
|
||||
"System": "System"
|
||||
"System": "System",
|
||||
|
||||
"Confirm success header": "Benutzer aktiviert!",
|
||||
"Confirm error header": "Benutzer existiert nicht oder ist schon aktiviert.",
|
||||
"Confirm pending header": "Benutzer wird aktiviert...",
|
||||
"Back to main": "Zurück zur Hauptseite",
|
||||
|
||||
"Page not found": "Die Seite konnte nicht gefunden werden.",
|
||||
"Session expired": "Deine Sitzung ist abgelaufen.",
|
||||
"General error": "Da ist wohl was schief gelaufen."
|
||||
}
|
||||
|
||||
@ -87,5 +87,14 @@
|
||||
|
||||
"Dark": "Dark",
|
||||
"Light": "Light",
|
||||
"System": "System"
|
||||
"System": "System",
|
||||
|
||||
"Confirm success header": "User confirmed!",
|
||||
"Confirm error header": "User does not exist or is already confirmed.",
|
||||
"confirm pending header": "User is getting confirmed...",
|
||||
"Back to main": "Back to the front page",
|
||||
|
||||
"Page not found": "This page was not found",
|
||||
"Session expired": "Your session has expired.",
|
||||
"General error": "Looks like something went wrong."
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@ interface ApiContext {
|
||||
|
||||
user?: (id?: number) => Promise<User>;
|
||||
createUser?: (data: UserCreate) => Promise<User>;
|
||||
confirmUser?: (code: string) => Promise<User>;
|
||||
updateUser?: (data: UserUpdate, id?: number) => Promise<User>;
|
||||
updateUserImage?: (data: UserImageUpdate, id?: number) => Promise<User>;
|
||||
|
||||
@ -48,6 +49,7 @@ export const useApi = () => {
|
||||
|
||||
user,
|
||||
createUser,
|
||||
confirmUser,
|
||||
updateUser,
|
||||
updateUserImage,
|
||||
|
||||
@ -63,6 +65,7 @@ export const useApi = () => {
|
||||
deletePost &&
|
||||
user &&
|
||||
createUser &&
|
||||
confirmUser &&
|
||||
updateUser &&
|
||||
updateUserImage &&
|
||||
userPosts
|
||||
@ -81,6 +84,7 @@ export const useApi = () => {
|
||||
|
||||
user,
|
||||
createUser,
|
||||
confirmUser,
|
||||
updateUser,
|
||||
updateUserImage,
|
||||
|
||||
@ -146,7 +150,7 @@ export const ApiProvider: FC<PropsWithChildren<Record<string, unknown>>> = ({ ch
|
||||
};
|
||||
|
||||
const updatePost = async (data: PostUpdate, id: number): Promise<PostAuth> => {
|
||||
return await (await reAuth(() => patch(`posts/${id}`, data as Record<string, unknown>))).json();
|
||||
return await (await reAuth(() => patchAuth(`posts/${id}`, data as Record<string, unknown>))).json();
|
||||
};
|
||||
|
||||
const deletePost = async (id: number): Promise<PostDelete> => {
|
||||
@ -161,8 +165,14 @@ export const ApiProvider: FC<PropsWithChildren<Record<string, unknown>>> = ({ ch
|
||||
return await (await post(`register`, data as unknown as Record<string, unknown>)).json();
|
||||
};
|
||||
|
||||
const confirmUser = async (code: string): Promise<User> => {
|
||||
return await (await patch(`register`, { code })).json();
|
||||
};
|
||||
|
||||
const updateUser = async (data: UserUpdate, id?: number): Promise<User> => {
|
||||
const _user = await (await reAuth(() => patch(`users/${id ?? 'self'}`, data as Record<string, unknown>))).json();
|
||||
const _user = await (
|
||||
await reAuth(() => patchAuth(`users/${id ?? 'self'}`, data as Record<string, unknown>))
|
||||
).json();
|
||||
setAuthenticatedUser(_user);
|
||||
return _user;
|
||||
};
|
||||
@ -247,6 +257,17 @@ export const ApiProvider: FC<PropsWithChildren<Record<string, unknown>>> = ({ ch
|
||||
};
|
||||
|
||||
const patch = async (endpoint: string, body?: Record<string, unknown>, headers?: HeadersInit) => {
|
||||
const response = await fetch(`${BASE}${endpoint}`, {
|
||||
mode: 'cors',
|
||||
method: 'patch',
|
||||
headers: headers,
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (response.ok) return response;
|
||||
throw await response.json();
|
||||
};
|
||||
|
||||
const patchAuth = async (endpoint: string, body?: Record<string, unknown>, headers?: HeadersInit) => {
|
||||
const response = await fetch(`${BASE}${endpoint}`, {
|
||||
mode: 'cors',
|
||||
method: 'patch',
|
||||
@ -313,6 +334,7 @@ export const ApiProvider: FC<PropsWithChildren<Record<string, unknown>>> = ({ ch
|
||||
|
||||
user,
|
||||
createUser,
|
||||
confirmUser,
|
||||
updateUser,
|
||||
updateUserImage,
|
||||
|
||||
|
||||
52
exam/react/src/components/Error/ErrorRouterComponent.tsx
Normal file
52
exam/react/src/components/Error/ErrorRouterComponent.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
import { ErrorOutline } from '@mui/icons-material';
|
||||
import { Grid, Link as MUILink, Typography } from '@mui/material';
|
||||
import { useQueryErrorResetBoundary } from '@tanstack/react-query';
|
||||
import { ErrorRouteComponent as TSErrorRouteComponent, useNavigate, useRouter } from '@tanstack/react-router';
|
||||
import { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import HeaderlessLayout from '../../components/Layouts/HeaderlessLayout';
|
||||
import { ROUTES } from '../../types/Routes';
|
||||
import { ERRORS } from './Errors';
|
||||
|
||||
const ErrorRouterComponent: TSErrorRouteComponent = ({ error }) => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const navigate = useNavigate();
|
||||
const queryErrorResetBoundary = useQueryErrorResetBoundary();
|
||||
|
||||
useEffect(() => {
|
||||
// Reset the query error boundary
|
||||
console.log(queryErrorResetBoundary.isReset());
|
||||
queryErrorResetBoundary.reset();
|
||||
}, [queryErrorResetBoundary]);
|
||||
|
||||
return (
|
||||
<HeaderlessLayout>
|
||||
<Grid container spacing={2} sx={{ marginTop: 0 }}>
|
||||
<Grid item xs={12} sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<ErrorOutline sx={{ fontSize: '200px' }} />
|
||||
</Grid>
|
||||
<Grid item xs={12} sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<Typography variant="h4" sx={{ textAlign: 'center' }}>
|
||||
{t('code' in error && error.code === ERRORS.UNAUTHORIZED ? 'Session expired' : 'General error')}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<MUILink
|
||||
variant="h6"
|
||||
underline="none"
|
||||
onClick={() => {
|
||||
console.log('CLICK AS WELL');
|
||||
router.invalidate();
|
||||
navigate({ to: ROUTES.INDEX });
|
||||
}}
|
||||
>
|
||||
{t('Back to main')}
|
||||
</MUILink>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</HeaderlessLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default ErrorRouterComponent;
|
||||
20
exam/react/src/components/Layouts/HeaderLayout.tsx
Normal file
20
exam/react/src/components/Layouts/HeaderLayout.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import { Box } from '@mui/material';
|
||||
import { ReactNode } from '@tanstack/react-router';
|
||||
import { FC } from 'react';
|
||||
import Footer from '../Footer/Footer';
|
||||
import Header from '../Header/Header';
|
||||
|
||||
const HeaderLayout: FC<{ children?: ReactNode }> = ({ children }) => {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<Box sx={{ maxWidth: '800px', flexGrow: 1 }}>{children}</Box>
|
||||
</Box>
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeaderLayout;
|
||||
18
exam/react/src/components/Layouts/HeaderlessLayout.tsx
Normal file
18
exam/react/src/components/Layouts/HeaderlessLayout.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import { Box } from '@mui/material';
|
||||
import { FC, ReactNode } from 'react';
|
||||
import Footer from '../Footer/Footer';
|
||||
|
||||
const HeaderlessLayout: FC<{ children?: ReactNode }> = ({ children }) => {
|
||||
return (
|
||||
<>
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<Box sx={{ maxWidth: '800px', flexGrow: 1 }}>{children}</Box>
|
||||
</Box>
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeaderlessLayout;
|
||||
31
exam/react/src/components/NotFound/NotFoundComponent.tsx
Normal file
31
exam/react/src/components/NotFound/NotFoundComponent.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import { Grid, Link as MUILink, Typography } from '@mui/material';
|
||||
import { Link } from '@tanstack/react-router';
|
||||
import { FC } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import HeaderlessLayout from '../../components/Layouts/HeaderlessLayout';
|
||||
|
||||
const NotFoundComponent: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<HeaderlessLayout>
|
||||
<Grid container spacing={2} sx={{ marginTop: 0 }}>
|
||||
<Grid item xs={12} sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<Typography variant="404">404</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<Typography variant="h4" sx={{ textAlign: 'center' }}>
|
||||
{t('Page not found')}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<MUILink component={Link} to="/" variant="h6" underline="none">
|
||||
{t('Back to main')}
|
||||
</MUILink>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</HeaderlessLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotFoundComponent;
|
||||
@ -3,18 +3,15 @@ import { useApi } from '../api/Api';
|
||||
|
||||
export const profileSelfQueryOptions = (Api: ReturnType<typeof useApi>) =>
|
||||
queryOptions({
|
||||
queryKey: ['profile'],
|
||||
queryFn: async () => await Api.user(),
|
||||
queryKey: ['profile', { id: Api.authenticatedUser?.id }],
|
||||
queryFn: async () => ({
|
||||
user: await Api.user(),
|
||||
posts: await Api.userPosts(Api.authenticatedUser?.id ?? 0),
|
||||
}),
|
||||
});
|
||||
|
||||
export const profileQueryOptions = (Api: ReturnType<typeof useApi>, id?: number) =>
|
||||
queryOptions({
|
||||
queryKey: ['profile', { id }],
|
||||
queryFn: async () => await Api.user(id),
|
||||
});
|
||||
|
||||
export const profilePostsQueryOptions = (Api: ReturnType<typeof useApi>, id: number) =>
|
||||
queryOptions({
|
||||
queryKey: ['profilePosts', { id }],
|
||||
queryFn: async () => await Api.userPosts(id),
|
||||
queryFn: async () => ({ user: await Api.user(id), posts: await Api.userPosts(id) }),
|
||||
});
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
import { Route as rootRoute } from './routes/__root'
|
||||
import { Route as IndexImport } from './routes/index'
|
||||
import { Route as ProfileIndexImport } from './routes/profile/index'
|
||||
import { Route as ConfirmIndexImport } from './routes/confirm/index'
|
||||
import { Route as ProfileIdImport } from './routes/profile/$id'
|
||||
|
||||
// Create/Update Routes
|
||||
@ -27,6 +28,11 @@ const ProfileIndexRoute = ProfileIndexImport.update({
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const ConfirmIndexRoute = ConfirmIndexImport.update({
|
||||
path: '/confirm/',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const ProfileIdRoute = ProfileIdImport.update({
|
||||
path: '/profile/$id',
|
||||
getParentRoute: () => rootRoute,
|
||||
@ -50,6 +56,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof ProfileIdImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/confirm/': {
|
||||
id: '/confirm/'
|
||||
path: '/confirm'
|
||||
fullPath: '/confirm'
|
||||
preLoaderRoute: typeof ConfirmIndexImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/profile/': {
|
||||
id: '/profile/'
|
||||
path: '/profile'
|
||||
@ -65,6 +78,7 @@ declare module '@tanstack/react-router' {
|
||||
export const routeTree = rootRoute.addChildren({
|
||||
IndexRoute,
|
||||
ProfileIdRoute,
|
||||
ConfirmIndexRoute,
|
||||
ProfileIndexRoute,
|
||||
})
|
||||
|
||||
@ -78,6 +92,7 @@ export const routeTree = rootRoute.addChildren({
|
||||
"children": [
|
||||
"/",
|
||||
"/profile/$id",
|
||||
"/confirm/",
|
||||
"/profile/"
|
||||
]
|
||||
},
|
||||
@ -87,6 +102,9 @@ export const routeTree = rootRoute.addChildren({
|
||||
"/profile/$id": {
|
||||
"filePath": "profile/$id.tsx"
|
||||
},
|
||||
"/confirm/": {
|
||||
"filePath": "confirm/index.tsx"
|
||||
},
|
||||
"/profile/": {
|
||||
"filePath": "profile/index.tsx"
|
||||
}
|
||||
|
||||
@ -1,53 +1,22 @@
|
||||
import { Box } from '@mui/material';
|
||||
import { QueryClient, useQueryErrorResetBoundary } from '@tanstack/react-query';
|
||||
import { createRootRouteWithContext, ErrorRouteComponent, Outlet, useRouter } from '@tanstack/react-router';
|
||||
import { QueryClient } from '@tanstack/react-query';
|
||||
import { createRootRouteWithContext, Outlet } from '@tanstack/react-router';
|
||||
import { TanStackRouterDevtools } from '@tanstack/router-devtools';
|
||||
import { useEffect } from 'react';
|
||||
import { useApi } from '../api/Api';
|
||||
import Footer from '../components/Footer/Footer';
|
||||
import Header from '../components/Header/Header';
|
||||
import ErrorRouterComponent from '../components/Error/ErrorRouterComponent';
|
||||
import NotFoundComponent from '../components/NotFound/NotFoundComponent';
|
||||
|
||||
const Root = () => {
|
||||
return (
|
||||
<Box sx={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
|
||||
<Header />
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<Box sx={{ maxWidth: '800px', flexGrow: 1 }}>
|
||||
<Outlet />
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
<Footer />
|
||||
<Outlet />
|
||||
{process.env.NODE_ENV === 'development' && <TanStackRouterDevtools />}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const ErrorComponent: ErrorRouteComponent = ({ error }) => {
|
||||
const router = useRouter();
|
||||
const queryErrorResetBoundary = useQueryErrorResetBoundary();
|
||||
|
||||
useEffect(() => {
|
||||
// Reset the query error boundary
|
||||
queryErrorResetBoundary.reset();
|
||||
}, [queryErrorResetBoundary]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{error.message}
|
||||
<button
|
||||
onClick={() => {
|
||||
// Invalidate the route to reload the loader, and reset any router error boundaries
|
||||
router.invalidate();
|
||||
}}
|
||||
>
|
||||
retry
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createRootRouteWithContext<{ queryClient: QueryClient; Api: ReturnType<typeof useApi> }>()({
|
||||
component: Root,
|
||||
errorComponent: ErrorComponent,
|
||||
notFoundComponent: NotFoundComponent,
|
||||
errorComponent: ErrorRouterComponent,
|
||||
});
|
||||
|
||||
71
exam/react/src/routes/confirm/index.tsx
Normal file
71
exam/react/src/routes/confirm/index.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
import { Done, Error } from '@mui/icons-material';
|
||||
import { CircularProgress, Grid, Link as MUILink, Typography } from '@mui/material';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { createFileRoute, Link, useNavigate } from '@tanstack/react-router';
|
||||
import { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useApi } from '../../api/Api';
|
||||
import HeaderlessLayout from '../../components/Layouts/HeaderlessLayout';
|
||||
import { ROUTES } from '../../types/Routes';
|
||||
|
||||
const Home = () => {
|
||||
const Api = useApi();
|
||||
const { code } = Route.useSearch();
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const confirmMutation = useMutation({
|
||||
mutationFn: ({ code: _code }: { code: string }) => {
|
||||
return Api.confirmUser(_code);
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (code) setTimeout(() => confirmMutation.mutate({ code }), 1000);
|
||||
}, []); //eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
useEffect(() => {
|
||||
if (!code) {
|
||||
navigate({ to: '/' });
|
||||
}
|
||||
}, [code]); //eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return (
|
||||
<HeaderlessLayout>
|
||||
<Grid container spacing={2} sx={{ marginTop: 0 }}>
|
||||
<Grid item xs={12} sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
{confirmMutation.isSuccess && <Done color="action" sx={{ fontSize: '200px' }} />}
|
||||
{confirmMutation.isError && <Error color="action" sx={{ fontSize: '200px' }} />}
|
||||
{(confirmMutation.isPending || confirmMutation.isIdle) && <CircularProgress size={200} />}
|
||||
</Grid>
|
||||
<Grid item xs={12} sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
{confirmMutation.isSuccess && <Typography variant="h5">{t('Confirm success header')}</Typography>}
|
||||
{confirmMutation.isError && (
|
||||
<Typography variant="h5" color="error">
|
||||
{t('Confirm error header')}
|
||||
</Typography>
|
||||
)}
|
||||
{(confirmMutation.isPending || confirmMutation.isIdle) && (
|
||||
<Typography variant="h5">{t('Confirm pending header')}</Typography>
|
||||
)}
|
||||
</Grid>
|
||||
{!confirmMutation.isPending && !confirmMutation.isIdle && (
|
||||
<Grid item xs={12} sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<MUILink component={Link} to="/" variant="h6" underline="none">
|
||||
{t('Back to main')}
|
||||
</MUILink>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</HeaderlessLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute(`${ROUTES.CONFIRM}/`)({
|
||||
validateSearch: (search: Record<string, unknown>): { code?: string } => {
|
||||
return {
|
||||
code: search?.code !== undefined ? (search?.code as string) : undefined,
|
||||
};
|
||||
},
|
||||
component: Home,
|
||||
});
|
||||
@ -14,6 +14,7 @@ import { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useApi } from '../api/Api';
|
||||
import PostForm from '../components/Forms/Post/PostForm';
|
||||
import HeaderLayout from '../components/Layouts/HeaderLayout';
|
||||
import Post from '../components/Post/Post';
|
||||
import { postsQueryOptions } from '../queries/postsQuery';
|
||||
import { ROUTES } from '../types/Routes';
|
||||
@ -34,7 +35,7 @@ const Home = () => {
|
||||
}, [page]); //eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeaderLayout>
|
||||
<Snackbar open={isFetching} message={t('Updating')} />
|
||||
<Grid container spacing={2} sx={{ marginTop: 0 }}>
|
||||
{postsQuery.data.map((post) => (
|
||||
@ -74,7 +75,7 @@ const Home = () => {
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</>
|
||||
</HeaderLayout>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -4,8 +4,9 @@ import { createFileRoute, redirect } from '@tanstack/react-router';
|
||||
import { t } from 'i18next';
|
||||
import { useApi } from '../../api/Api';
|
||||
import { ERRORS } from '../../components/Error/Errors';
|
||||
import HeaderLayout from '../../components/Layouts/HeaderLayout';
|
||||
import Profile from '../../components/Profile/Profile';
|
||||
import { profilePostsQueryOptions, profileQueryOptions } from '../../queries/profileQuery';
|
||||
import { profileQueryOptions } from '../../queries/profileQuery';
|
||||
import { PostAuth } from '../../types/Post';
|
||||
import { ROUTES } from '../../types/Routes';
|
||||
|
||||
@ -13,40 +14,24 @@ const ProfilePage = () => {
|
||||
const Api = useApi();
|
||||
const { id } = Route.useParams();
|
||||
const {
|
||||
data: profileQuery,
|
||||
isFetching: isFetchingProfile,
|
||||
error: errorProfile,
|
||||
failureReason: failureReasonProfile,
|
||||
} = useSuspenseQuery(profileQueryOptions(Api));
|
||||
const {
|
||||
data: profilePostsQuery,
|
||||
isFetching: isFetchingPosts,
|
||||
error: errorPosts,
|
||||
failureReason: failureReasonPosts,
|
||||
} = useSuspenseQuery(profilePostsQueryOptions(Api, id));
|
||||
data: { user, posts },
|
||||
isFetching,
|
||||
error,
|
||||
failureReason,
|
||||
} = useSuspenseQuery(profileQueryOptions(Api, id));
|
||||
|
||||
if (failureReasonProfile && 'code' in failureReasonProfile && failureReasonProfile.code === ERRORS.UNAUTHORIZED) {
|
||||
throw failureReasonProfile;
|
||||
if (failureReason && 'code' in failureReason && failureReason.code === ERRORS.UNAUTHORIZED) {
|
||||
throw failureReason;
|
||||
}
|
||||
if (errorProfile && 'code' in errorProfile && errorProfile.code === ERRORS.UNAUTHORIZED) {
|
||||
throw errorProfile;
|
||||
}
|
||||
if (failureReasonPosts && 'code' in failureReasonPosts && failureReasonPosts.code === ERRORS.UNAUTHORIZED) {
|
||||
throw failureReasonPosts;
|
||||
}
|
||||
if (errorPosts && 'code' in errorPosts && errorPosts.code === ERRORS.UNAUTHORIZED) {
|
||||
throw errorPosts;
|
||||
if (error && 'code' in error && error.code === ERRORS.UNAUTHORIZED) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Snackbar open={isFetchingProfile || isFetchingPosts} message={t('Updating')} />
|
||||
<Profile
|
||||
user={profileQuery}
|
||||
posts={profilePostsQuery.data as PostAuth[]}
|
||||
canEdit={Api.authenticatedUser?.isAdmin}
|
||||
/>
|
||||
</>
|
||||
<HeaderLayout>
|
||||
<Snackbar open={isFetching} message={t('Updating')} />
|
||||
<Profile user={user} posts={posts.data as PostAuth[]} canEdit={Api.authenticatedUser?.isAdmin} />
|
||||
</HeaderLayout>
|
||||
);
|
||||
};
|
||||
|
||||
@ -57,7 +42,6 @@ export const Route = createFileRoute(`${ROUTES.PROFILE}/$id`)({
|
||||
},
|
||||
loader: ({ context: { queryClient, Api }, params: { id } }) => {
|
||||
queryClient.ensureQueryData(profileQueryOptions(Api, id));
|
||||
queryClient.ensureQueryData(profilePostsQueryOptions(Api, id));
|
||||
},
|
||||
beforeLoad: ({ params: { id }, context: { Api } }) => {
|
||||
if (!Api.hasAuth) throw redirect({ to: ROUTES.INDEX });
|
||||
|
||||
@ -4,51 +4,39 @@ import { createFileRoute, redirect } from '@tanstack/react-router';
|
||||
import { t } from 'i18next';
|
||||
import { useApi } from '../../api/Api';
|
||||
import { ERRORS } from '../../components/Error/Errors';
|
||||
import HeaderLayout from '../../components/Layouts/HeaderLayout';
|
||||
import Profile from '../../components/Profile/Profile';
|
||||
import { profilePostsQueryOptions, profileSelfQueryOptions } from '../../queries/profileQuery';
|
||||
import { profileSelfQueryOptions } from '../../queries/profileQuery';
|
||||
import { PostAuth } from '../../types/Post';
|
||||
import { ROUTES } from '../../types/Routes';
|
||||
|
||||
const ProfilePage = () => {
|
||||
const Api = useApi();
|
||||
const {
|
||||
data: profileQuery,
|
||||
isFetching: isFetchingProfile,
|
||||
error: errorProfile,
|
||||
failureReason: failureReasonProfile,
|
||||
data: { user, posts },
|
||||
isFetching,
|
||||
error,
|
||||
failureReason,
|
||||
} = useSuspenseQuery(profileSelfQueryOptions(Api));
|
||||
const {
|
||||
data: profilePostsQuery,
|
||||
isFetching: isFetchingPosts,
|
||||
error: errorPosts,
|
||||
failureReason: failureReasonPosts,
|
||||
} = useSuspenseQuery(profilePostsQueryOptions(Api, Api.authenticatedUser?.id ?? 0));
|
||||
|
||||
if (failureReasonProfile && 'code' in failureReasonProfile && failureReasonProfile.code === ERRORS.UNAUTHORIZED) {
|
||||
throw failureReasonProfile;
|
||||
if (failureReason && 'code' in failureReason && failureReason.code === ERRORS.UNAUTHORIZED) {
|
||||
throw failureReason;
|
||||
}
|
||||
if (errorProfile && 'code' in errorProfile && errorProfile.code === ERRORS.UNAUTHORIZED) {
|
||||
throw errorProfile;
|
||||
}
|
||||
if (failureReasonPosts && 'code' in failureReasonPosts && failureReasonPosts.code === ERRORS.UNAUTHORIZED) {
|
||||
throw failureReasonPosts;
|
||||
}
|
||||
if (errorPosts && 'code' in errorPosts && errorPosts.code === ERRORS.UNAUTHORIZED) {
|
||||
throw errorPosts;
|
||||
if (error && 'code' in error && error.code === ERRORS.UNAUTHORIZED) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Snackbar open={isFetchingProfile || isFetchingPosts} message={t('Updating')} />
|
||||
<Profile user={profileQuery} posts={profilePostsQuery.data as PostAuth[]} canEdit={true} />
|
||||
</>
|
||||
<HeaderLayout>
|
||||
<Snackbar open={isFetching} message={t('Updating')} />
|
||||
<Profile user={user} posts={posts.data as PostAuth[]} canEdit={true} />
|
||||
</HeaderLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute(`${ROUTES.PROFILE}/`)({
|
||||
loader: ({ context: { queryClient, Api } }) => {
|
||||
queryClient.ensureQueryData(profileSelfQueryOptions(Api));
|
||||
queryClient.ensureQueryData(profilePostsQueryOptions(Api, Api.authenticatedUser?.id ?? 0));
|
||||
},
|
||||
beforeLoad: ({ context: { Api } }) => {
|
||||
if (!Api.hasAuth) throw redirect({ to: ROUTES.INDEX });
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { createTheme } from '@mui/material';
|
||||
import typography from '../overrides/typography';
|
||||
import MuiSnackbarContent from '../overrides/MuiSnackbarContent';
|
||||
|
||||
const darkTheme = createTheme({
|
||||
let darkTheme = createTheme({
|
||||
palette: {
|
||||
mode: 'dark',
|
||||
primary: {
|
||||
@ -26,15 +28,14 @@ const darkTheme = createTheme({
|
||||
paper: '#0d1019',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
darkTheme = createTheme(darkTheme, {
|
||||
typography: {
|
||||
...typography(darkTheme),
|
||||
},
|
||||
components: {
|
||||
MuiSnackbarContent: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundColor: undefined,
|
||||
color: 'text.primary',
|
||||
},
|
||||
},
|
||||
},
|
||||
...MuiSnackbarContent,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { createTheme } from '@mui/material';
|
||||
import typography from '../overrides/typography';
|
||||
import MuiSnackbarContent from '../overrides/MuiSnackbarContent';
|
||||
|
||||
const lightTheme = createTheme({
|
||||
let lightTheme = createTheme({
|
||||
palette: {
|
||||
mode: 'light',
|
||||
primary: {
|
||||
@ -26,15 +28,14 @@ const lightTheme = createTheme({
|
||||
paper: '#ffffff',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
lightTheme = createTheme(lightTheme, {
|
||||
typography: {
|
||||
...typography(lightTheme),
|
||||
},
|
||||
components: {
|
||||
MuiSnackbarContent: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundColor: undefined,
|
||||
color: 'text.primary',
|
||||
},
|
||||
},
|
||||
},
|
||||
...MuiSnackbarContent,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
12
exam/react/src/theme/overrides/MuiSnackbarContent.ts
Normal file
12
exam/react/src/theme/overrides/MuiSnackbarContent.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { Components } from '@mui/material';
|
||||
|
||||
const MuiSnackbarContent: Components['MuiSnackbarContent'] = {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundColor: undefined,
|
||||
color: 'text.primary',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default MuiSnackbarContent;
|
||||
34
exam/react/src/theme/overrides/typography.ts
Normal file
34
exam/react/src/theme/overrides/typography.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { Palette, Theme } from '@mui/material';
|
||||
import { TypographyOptions } from '@mui/material/styles/createTypography';
|
||||
|
||||
declare module '@mui/material/styles' {
|
||||
interface TypographyVariants {
|
||||
'404': React.CSSProperties;
|
||||
}
|
||||
|
||||
// allow configuration using `createTheme`
|
||||
interface TypographyVariantsOptions {
|
||||
'404'?: React.CSSProperties;
|
||||
}
|
||||
}
|
||||
|
||||
// Update the Typography's variant prop options
|
||||
declare module '@mui/material/Typography' {
|
||||
interface TypographyPropsVariantOverrides {
|
||||
'404': true;
|
||||
}
|
||||
}
|
||||
|
||||
const typography = (theme: Theme): TypographyOptions | ((palette: Palette) => TypographyOptions) => ({
|
||||
'404': {
|
||||
fontSize: '6rem',
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
fontSize: '12rem',
|
||||
},
|
||||
[theme.breakpoints.up('md')]: {
|
||||
fontSize: '18rem',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default typography;
|
||||
@ -1,4 +1,5 @@
|
||||
export enum ROUTES {
|
||||
INDEX = '/',
|
||||
PROFILE = '/profile',
|
||||
CONFIRM = '/confirm',
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user