ErrorComponent, 404Component, Theming

This commit is contained in:
2024-07-29 16:03:27 +02:00
parent e6fcd54e22
commit bf2f4954ee
27 changed files with 412 additions and 156 deletions
+10 -1
View File
@@ -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."
}
+10 -1
View File
@@ -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."
}
+24 -2
View File
@@ -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,
@@ -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;
@@ -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;
@@ -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;
@@ -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;
+6 -9
View File
@@ -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) }),
});
+18
View File
@@ -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"
}
+7 -38
View File
@@ -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
View 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,
});
+3 -2
View File
@@ -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>
);
};
+15 -31
View File
@@ -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 });
+14 -26
View File
@@ -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 });
+10 -9
View File
@@ -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,
},
});
+10 -9
View File
@@ -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,
},
});
@@ -0,0 +1,12 @@
import { Components } from '@mui/material';
const MuiSnackbarContent: Components['MuiSnackbarContent'] = {
styleOverrides: {
root: {
backgroundColor: undefined,
color: 'text.primary',
},
},
};
export default MuiSnackbarContent;
@@ -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
View File
@@ -1,4 +1,5 @@
export enum ROUTES {
INDEX = '/',
PROFILE = '/profile',
CONFIRM = '/confirm',
}