ErrorComponent, 404Component, Theming

This commit is contained in:
Kilian Hofmann 2024-07-29 16:03:27 +02:00
parent e6fcd54e22
commit bf2f4954ee
27 changed files with 412 additions and 156 deletions

File diff suppressed because one or more lines are too long

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

View File

@ -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">

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."
}

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."
}

File diff suppressed because one or more lines are too long

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."
}

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."
}

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,

View 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;

View 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;

View 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;

View 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;

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) }),
});

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

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,
});

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,
});

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>
);
};

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 });

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 });

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,
},
});

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,
},
});

View File

@ -0,0 +1,12 @@
import { Components } from '@mui/material';
const MuiSnackbarContent: Components['MuiSnackbarContent'] = {
styleOverrides: {
root: {
backgroundColor: undefined,
color: 'text.primary',
},
},
};
export default MuiSnackbarContent;

View 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;

View File

@ -1,4 +1,5 @@
export enum ROUTES {
INDEX = '/',
PROFILE = '/profile',
CONFIRM = '/confirm',
}