ErrorComponent, 404Component, Theming
This commit is contained in:
Vendored
-5
File diff suppressed because one or more lines are too long
Vendored
+5
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
-2
File diff suppressed because one or more lines are too long
Vendored
+3
-3
@@ -5,10 +5,10 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/phpCourse/exam/dist/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/phpCourse/exam/dist/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Vite + React + TS</title>
|
<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/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/mui-4z7lf7t1.js">
|
||||||
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/tanstack-DojtBDN6.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/zustand-DAXCIHlT.js">
|
||||||
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/i18n-W-kxdzA-.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">
|
<link rel="stylesheet" crossorigin href="/phpCourse/exam/dist/assets/mui-CKDNpdid.css">
|
||||||
|
|||||||
+10
-1
@@ -86,5 +86,14 @@
|
|||||||
|
|
||||||
"Dark": "Dunkel",
|
"Dark": "Dunkel",
|
||||||
"Light": "Hell",
|
"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
@@ -87,5 +87,14 @@
|
|||||||
|
|
||||||
"Dark": "Dark",
|
"Dark": "Dark",
|
||||||
"Light": "Light",
|
"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."
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -86,5 +86,14 @@
|
|||||||
|
|
||||||
"Dark": "Dunkel",
|
"Dark": "Dunkel",
|
||||||
"Light": "Hell",
|
"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",
|
"Dark": "Dark",
|
||||||
"Light": "Light",
|
"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>;
|
user?: (id?: number) => Promise<User>;
|
||||||
createUser?: (data: UserCreate) => Promise<User>;
|
createUser?: (data: UserCreate) => Promise<User>;
|
||||||
|
confirmUser?: (code: string) => Promise<User>;
|
||||||
updateUser?: (data: UserUpdate, id?: number) => Promise<User>;
|
updateUser?: (data: UserUpdate, id?: number) => Promise<User>;
|
||||||
updateUserImage?: (data: UserImageUpdate, id?: number) => Promise<User>;
|
updateUserImage?: (data: UserImageUpdate, id?: number) => Promise<User>;
|
||||||
|
|
||||||
@@ -48,6 +49,7 @@ export const useApi = () => {
|
|||||||
|
|
||||||
user,
|
user,
|
||||||
createUser,
|
createUser,
|
||||||
|
confirmUser,
|
||||||
updateUser,
|
updateUser,
|
||||||
updateUserImage,
|
updateUserImage,
|
||||||
|
|
||||||
@@ -63,6 +65,7 @@ export const useApi = () => {
|
|||||||
deletePost &&
|
deletePost &&
|
||||||
user &&
|
user &&
|
||||||
createUser &&
|
createUser &&
|
||||||
|
confirmUser &&
|
||||||
updateUser &&
|
updateUser &&
|
||||||
updateUserImage &&
|
updateUserImage &&
|
||||||
userPosts
|
userPosts
|
||||||
@@ -81,6 +84,7 @@ export const useApi = () => {
|
|||||||
|
|
||||||
user,
|
user,
|
||||||
createUser,
|
createUser,
|
||||||
|
confirmUser,
|
||||||
updateUser,
|
updateUser,
|
||||||
updateUserImage,
|
updateUserImage,
|
||||||
|
|
||||||
@@ -146,7 +150,7 @@ export const ApiProvider: FC<PropsWithChildren<Record<string, unknown>>> = ({ ch
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updatePost = async (data: PostUpdate, id: number): Promise<PostAuth> => {
|
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> => {
|
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();
|
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 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);
|
setAuthenticatedUser(_user);
|
||||||
return _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 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}`, {
|
const response = await fetch(`${BASE}${endpoint}`, {
|
||||||
mode: 'cors',
|
mode: 'cors',
|
||||||
method: 'patch',
|
method: 'patch',
|
||||||
@@ -313,6 +334,7 @@ export const ApiProvider: FC<PropsWithChildren<Record<string, unknown>>> = ({ ch
|
|||||||
|
|
||||||
user,
|
user,
|
||||||
createUser,
|
createUser,
|
||||||
|
confirmUser,
|
||||||
updateUser,
|
updateUser,
|
||||||
updateUserImage,
|
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;
|
||||||
@@ -3,18 +3,15 @@ import { useApi } from '../api/Api';
|
|||||||
|
|
||||||
export const profileSelfQueryOptions = (Api: ReturnType<typeof useApi>) =>
|
export const profileSelfQueryOptions = (Api: ReturnType<typeof useApi>) =>
|
||||||
queryOptions({
|
queryOptions({
|
||||||
queryKey: ['profile'],
|
queryKey: ['profile', { id: Api.authenticatedUser?.id }],
|
||||||
queryFn: async () => await Api.user(),
|
queryFn: async () => ({
|
||||||
|
user: await Api.user(),
|
||||||
|
posts: await Api.userPosts(Api.authenticatedUser?.id ?? 0),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const profileQueryOptions = (Api: ReturnType<typeof useApi>, id?: number) =>
|
export const profileQueryOptions = (Api: ReturnType<typeof useApi>, id?: number) =>
|
||||||
queryOptions({
|
queryOptions({
|
||||||
queryKey: ['profile', { id }],
|
queryKey: ['profile', { id }],
|
||||||
queryFn: async () => await Api.user(id),
|
queryFn: async () => ({ user: await Api.user(id), posts: await Api.userPosts(id) }),
|
||||||
});
|
|
||||||
|
|
||||||
export const profilePostsQueryOptions = (Api: ReturnType<typeof useApi>, id: number) =>
|
|
||||||
queryOptions({
|
|
||||||
queryKey: ['profilePosts', { id }],
|
|
||||||
queryFn: async () => await Api.userPosts(id),
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
import { Route as rootRoute } from './routes/__root'
|
import { Route as rootRoute } from './routes/__root'
|
||||||
import { Route as IndexImport } from './routes/index'
|
import { Route as IndexImport } from './routes/index'
|
||||||
import { Route as ProfileIndexImport } from './routes/profile/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'
|
import { Route as ProfileIdImport } from './routes/profile/$id'
|
||||||
|
|
||||||
// Create/Update Routes
|
// Create/Update Routes
|
||||||
@@ -27,6 +28,11 @@ const ProfileIndexRoute = ProfileIndexImport.update({
|
|||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
|
const ConfirmIndexRoute = ConfirmIndexImport.update({
|
||||||
|
path: '/confirm/',
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
const ProfileIdRoute = ProfileIdImport.update({
|
const ProfileIdRoute = ProfileIdImport.update({
|
||||||
path: '/profile/$id',
|
path: '/profile/$id',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
@@ -50,6 +56,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof ProfileIdImport
|
preLoaderRoute: typeof ProfileIdImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
|
'/confirm/': {
|
||||||
|
id: '/confirm/'
|
||||||
|
path: '/confirm'
|
||||||
|
fullPath: '/confirm'
|
||||||
|
preLoaderRoute: typeof ConfirmIndexImport
|
||||||
|
parentRoute: typeof rootRoute
|
||||||
|
}
|
||||||
'/profile/': {
|
'/profile/': {
|
||||||
id: '/profile/'
|
id: '/profile/'
|
||||||
path: '/profile'
|
path: '/profile'
|
||||||
@@ -65,6 +78,7 @@ declare module '@tanstack/react-router' {
|
|||||||
export const routeTree = rootRoute.addChildren({
|
export const routeTree = rootRoute.addChildren({
|
||||||
IndexRoute,
|
IndexRoute,
|
||||||
ProfileIdRoute,
|
ProfileIdRoute,
|
||||||
|
ConfirmIndexRoute,
|
||||||
ProfileIndexRoute,
|
ProfileIndexRoute,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -78,6 +92,7 @@ export const routeTree = rootRoute.addChildren({
|
|||||||
"children": [
|
"children": [
|
||||||
"/",
|
"/",
|
||||||
"/profile/$id",
|
"/profile/$id",
|
||||||
|
"/confirm/",
|
||||||
"/profile/"
|
"/profile/"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -87,6 +102,9 @@ export const routeTree = rootRoute.addChildren({
|
|||||||
"/profile/$id": {
|
"/profile/$id": {
|
||||||
"filePath": "profile/$id.tsx"
|
"filePath": "profile/$id.tsx"
|
||||||
},
|
},
|
||||||
|
"/confirm/": {
|
||||||
|
"filePath": "confirm/index.tsx"
|
||||||
|
},
|
||||||
"/profile/": {
|
"/profile/": {
|
||||||
"filePath": "profile/index.tsx"
|
"filePath": "profile/index.tsx"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,53 +1,22 @@
|
|||||||
import { Box } from '@mui/material';
|
import { Box } from '@mui/material';
|
||||||
import { QueryClient, useQueryErrorResetBoundary } from '@tanstack/react-query';
|
import { QueryClient } from '@tanstack/react-query';
|
||||||
import { createRootRouteWithContext, ErrorRouteComponent, Outlet, useRouter } from '@tanstack/react-router';
|
import { createRootRouteWithContext, Outlet } from '@tanstack/react-router';
|
||||||
import { TanStackRouterDevtools } from '@tanstack/router-devtools';
|
import { TanStackRouterDevtools } from '@tanstack/router-devtools';
|
||||||
import { useEffect } from 'react';
|
|
||||||
import { useApi } from '../api/Api';
|
import { useApi } from '../api/Api';
|
||||||
import Footer from '../components/Footer/Footer';
|
import ErrorRouterComponent from '../components/Error/ErrorRouterComponent';
|
||||||
import Header from '../components/Header/Header';
|
import NotFoundComponent from '../components/NotFound/NotFoundComponent';
|
||||||
|
|
||||||
const Root = () => {
|
const Root = () => {
|
||||||
return (
|
return (
|
||||||
<Box sx={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
|
<Box sx={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
|
||||||
<Header />
|
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
|
||||||
<Box sx={{ maxWidth: '800px', flexGrow: 1 }}>
|
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
<Box sx={{ flexGrow: 1 }} />
|
|
||||||
<Footer />
|
|
||||||
{process.env.NODE_ENV === 'development' && <TanStackRouterDevtools />}
|
{process.env.NODE_ENV === 'development' && <TanStackRouterDevtools />}
|
||||||
</Box>
|
</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> }>()({
|
export const Route = createRootRouteWithContext<{ queryClient: QueryClient; Api: ReturnType<typeof useApi> }>()({
|
||||||
component: Root,
|
component: Root,
|
||||||
errorComponent: ErrorComponent,
|
notFoundComponent: NotFoundComponent,
|
||||||
|
errorComponent: ErrorRouterComponent,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 { useTranslation } from 'react-i18next';
|
||||||
import { useApi } from '../api/Api';
|
import { useApi } from '../api/Api';
|
||||||
import PostForm from '../components/Forms/Post/PostForm';
|
import PostForm from '../components/Forms/Post/PostForm';
|
||||||
|
import HeaderLayout from '../components/Layouts/HeaderLayout';
|
||||||
import Post from '../components/Post/Post';
|
import Post from '../components/Post/Post';
|
||||||
import { postsQueryOptions } from '../queries/postsQuery';
|
import { postsQueryOptions } from '../queries/postsQuery';
|
||||||
import { ROUTES } from '../types/Routes';
|
import { ROUTES } from '../types/Routes';
|
||||||
@@ -34,7 +35,7 @@ const Home = () => {
|
|||||||
}, [page]); //eslint-disable-line react-hooks/exhaustive-deps
|
}, [page]); //eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<HeaderLayout>
|
||||||
<Snackbar open={isFetching} message={t('Updating')} />
|
<Snackbar open={isFetching} message={t('Updating')} />
|
||||||
<Grid container spacing={2} sx={{ marginTop: 0 }}>
|
<Grid container spacing={2} sx={{ marginTop: 0 }}>
|
||||||
{postsQuery.data.map((post) => (
|
{postsQuery.data.map((post) => (
|
||||||
@@ -74,7 +75,7 @@ const Home = () => {
|
|||||||
</Grid>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
</>
|
</HeaderLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ import { createFileRoute, redirect } from '@tanstack/react-router';
|
|||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
import { useApi } from '../../api/Api';
|
import { useApi } from '../../api/Api';
|
||||||
import { ERRORS } from '../../components/Error/Errors';
|
import { ERRORS } from '../../components/Error/Errors';
|
||||||
|
import HeaderLayout from '../../components/Layouts/HeaderLayout';
|
||||||
import Profile from '../../components/Profile/Profile';
|
import Profile from '../../components/Profile/Profile';
|
||||||
import { profilePostsQueryOptions, profileQueryOptions } from '../../queries/profileQuery';
|
import { profileQueryOptions } from '../../queries/profileQuery';
|
||||||
import { PostAuth } from '../../types/Post';
|
import { PostAuth } from '../../types/Post';
|
||||||
import { ROUTES } from '../../types/Routes';
|
import { ROUTES } from '../../types/Routes';
|
||||||
|
|
||||||
@@ -13,40 +14,24 @@ const ProfilePage = () => {
|
|||||||
const Api = useApi();
|
const Api = useApi();
|
||||||
const { id } = Route.useParams();
|
const { id } = Route.useParams();
|
||||||
const {
|
const {
|
||||||
data: profileQuery,
|
data: { user, posts },
|
||||||
isFetching: isFetchingProfile,
|
isFetching,
|
||||||
error: errorProfile,
|
error,
|
||||||
failureReason: failureReasonProfile,
|
failureReason,
|
||||||
} = useSuspenseQuery(profileQueryOptions(Api));
|
} = useSuspenseQuery(profileQueryOptions(Api, id));
|
||||||
const {
|
|
||||||
data: profilePostsQuery,
|
|
||||||
isFetching: isFetchingPosts,
|
|
||||||
error: errorPosts,
|
|
||||||
failureReason: failureReasonPosts,
|
|
||||||
} = useSuspenseQuery(profilePostsQueryOptions(Api, id));
|
|
||||||
|
|
||||||
if (failureReasonProfile && 'code' in failureReasonProfile && failureReasonProfile.code === ERRORS.UNAUTHORIZED) {
|
if (failureReason && 'code' in failureReason && failureReason.code === ERRORS.UNAUTHORIZED) {
|
||||||
throw failureReasonProfile;
|
throw failureReason;
|
||||||
}
|
}
|
||||||
if (errorProfile && 'code' in errorProfile && errorProfile.code === ERRORS.UNAUTHORIZED) {
|
if (error && 'code' in error && error.code === ERRORS.UNAUTHORIZED) {
|
||||||
throw errorProfile;
|
throw error;
|
||||||
}
|
|
||||||
if (failureReasonPosts && 'code' in failureReasonPosts && failureReasonPosts.code === ERRORS.UNAUTHORIZED) {
|
|
||||||
throw failureReasonPosts;
|
|
||||||
}
|
|
||||||
if (errorPosts && 'code' in errorPosts && errorPosts.code === ERRORS.UNAUTHORIZED) {
|
|
||||||
throw errorPosts;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<HeaderLayout>
|
||||||
<Snackbar open={isFetchingProfile || isFetchingPosts} message={t('Updating')} />
|
<Snackbar open={isFetching} message={t('Updating')} />
|
||||||
<Profile
|
<Profile user={user} posts={posts.data as PostAuth[]} canEdit={Api.authenticatedUser?.isAdmin} />
|
||||||
user={profileQuery}
|
</HeaderLayout>
|
||||||
posts={profilePostsQuery.data as PostAuth[]}
|
|
||||||
canEdit={Api.authenticatedUser?.isAdmin}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -57,7 +42,6 @@ export const Route = createFileRoute(`${ROUTES.PROFILE}/$id`)({
|
|||||||
},
|
},
|
||||||
loader: ({ context: { queryClient, Api }, params: { id } }) => {
|
loader: ({ context: { queryClient, Api }, params: { id } }) => {
|
||||||
queryClient.ensureQueryData(profileQueryOptions(Api, id));
|
queryClient.ensureQueryData(profileQueryOptions(Api, id));
|
||||||
queryClient.ensureQueryData(profilePostsQueryOptions(Api, id));
|
|
||||||
},
|
},
|
||||||
beforeLoad: ({ params: { id }, context: { Api } }) => {
|
beforeLoad: ({ params: { id }, context: { Api } }) => {
|
||||||
if (!Api.hasAuth) throw redirect({ to: ROUTES.INDEX });
|
if (!Api.hasAuth) throw redirect({ to: ROUTES.INDEX });
|
||||||
|
|||||||
@@ -4,51 +4,39 @@ import { createFileRoute, redirect } from '@tanstack/react-router';
|
|||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
import { useApi } from '../../api/Api';
|
import { useApi } from '../../api/Api';
|
||||||
import { ERRORS } from '../../components/Error/Errors';
|
import { ERRORS } from '../../components/Error/Errors';
|
||||||
|
import HeaderLayout from '../../components/Layouts/HeaderLayout';
|
||||||
import Profile from '../../components/Profile/Profile';
|
import Profile from '../../components/Profile/Profile';
|
||||||
import { profilePostsQueryOptions, profileSelfQueryOptions } from '../../queries/profileQuery';
|
import { profileSelfQueryOptions } from '../../queries/profileQuery';
|
||||||
import { PostAuth } from '../../types/Post';
|
import { PostAuth } from '../../types/Post';
|
||||||
import { ROUTES } from '../../types/Routes';
|
import { ROUTES } from '../../types/Routes';
|
||||||
|
|
||||||
const ProfilePage = () => {
|
const ProfilePage = () => {
|
||||||
const Api = useApi();
|
const Api = useApi();
|
||||||
const {
|
const {
|
||||||
data: profileQuery,
|
data: { user, posts },
|
||||||
isFetching: isFetchingProfile,
|
isFetching,
|
||||||
error: errorProfile,
|
error,
|
||||||
failureReason: failureReasonProfile,
|
failureReason,
|
||||||
} = useSuspenseQuery(profileSelfQueryOptions(Api));
|
} = 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) {
|
if (failureReason && 'code' in failureReason && failureReason.code === ERRORS.UNAUTHORIZED) {
|
||||||
throw failureReasonProfile;
|
throw failureReason;
|
||||||
}
|
}
|
||||||
if (errorProfile && 'code' in errorProfile && errorProfile.code === ERRORS.UNAUTHORIZED) {
|
if (error && 'code' in error && error.code === ERRORS.UNAUTHORIZED) {
|
||||||
throw errorProfile;
|
throw error;
|
||||||
}
|
|
||||||
if (failureReasonPosts && 'code' in failureReasonPosts && failureReasonPosts.code === ERRORS.UNAUTHORIZED) {
|
|
||||||
throw failureReasonPosts;
|
|
||||||
}
|
|
||||||
if (errorPosts && 'code' in errorPosts && errorPosts.code === ERRORS.UNAUTHORIZED) {
|
|
||||||
throw errorPosts;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<HeaderLayout>
|
||||||
<Snackbar open={isFetchingProfile || isFetchingPosts} message={t('Updating')} />
|
<Snackbar open={isFetching} message={t('Updating')} />
|
||||||
<Profile user={profileQuery} posts={profilePostsQuery.data as PostAuth[]} canEdit={true} />
|
<Profile user={user} posts={posts.data as PostAuth[]} canEdit={true} />
|
||||||
</>
|
</HeaderLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Route = createFileRoute(`${ROUTES.PROFILE}/`)({
|
export const Route = createFileRoute(`${ROUTES.PROFILE}/`)({
|
||||||
loader: ({ context: { queryClient, Api } }) => {
|
loader: ({ context: { queryClient, Api } }) => {
|
||||||
queryClient.ensureQueryData(profileSelfQueryOptions(Api));
|
queryClient.ensureQueryData(profileSelfQueryOptions(Api));
|
||||||
queryClient.ensureQueryData(profilePostsQueryOptions(Api, Api.authenticatedUser?.id ?? 0));
|
|
||||||
},
|
},
|
||||||
beforeLoad: ({ context: { Api } }) => {
|
beforeLoad: ({ context: { Api } }) => {
|
||||||
if (!Api.hasAuth) throw redirect({ to: ROUTES.INDEX });
|
if (!Api.hasAuth) throw redirect({ to: ROUTES.INDEX });
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { createTheme } from '@mui/material';
|
import { createTheme } from '@mui/material';
|
||||||
|
import typography from '../overrides/typography';
|
||||||
|
import MuiSnackbarContent from '../overrides/MuiSnackbarContent';
|
||||||
|
|
||||||
const darkTheme = createTheme({
|
let darkTheme = createTheme({
|
||||||
palette: {
|
palette: {
|
||||||
mode: 'dark',
|
mode: 'dark',
|
||||||
primary: {
|
primary: {
|
||||||
@@ -26,15 +28,14 @@ const darkTheme = createTheme({
|
|||||||
paper: '#0d1019',
|
paper: '#0d1019',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
darkTheme = createTheme(darkTheme, {
|
||||||
|
typography: {
|
||||||
|
...typography(darkTheme),
|
||||||
|
},
|
||||||
components: {
|
components: {
|
||||||
MuiSnackbarContent: {
|
...MuiSnackbarContent,
|
||||||
styleOverrides: {
|
|
||||||
root: {
|
|
||||||
backgroundColor: undefined,
|
|
||||||
color: 'text.primary',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { createTheme } from '@mui/material';
|
import { createTheme } from '@mui/material';
|
||||||
|
import typography from '../overrides/typography';
|
||||||
|
import MuiSnackbarContent from '../overrides/MuiSnackbarContent';
|
||||||
|
|
||||||
const lightTheme = createTheme({
|
let lightTheme = createTheme({
|
||||||
palette: {
|
palette: {
|
||||||
mode: 'light',
|
mode: 'light',
|
||||||
primary: {
|
primary: {
|
||||||
@@ -26,15 +28,14 @@ const lightTheme = createTheme({
|
|||||||
paper: '#ffffff',
|
paper: '#ffffff',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
lightTheme = createTheme(lightTheme, {
|
||||||
|
typography: {
|
||||||
|
...typography(lightTheme),
|
||||||
|
},
|
||||||
components: {
|
components: {
|
||||||
MuiSnackbarContent: {
|
...MuiSnackbarContent,
|
||||||
styleOverrides: {
|
|
||||||
root: {
|
|
||||||
backgroundColor: undefined,
|
|
||||||
color: 'text.primary',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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,4 +1,5 @@
|
|||||||
export enum ROUTES {
|
export enum ROUTES {
|
||||||
INDEX = '/',
|
INDEX = '/',
|
||||||
PROFILE = '/profile',
|
PROFILE = '/profile',
|
||||||
|
CONFIRM = '/confirm',
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user