Skip to content

Commit 76e68bb

Browse files
committed
chore(tidy-code/UI): Clean up breadcrumbs
Signed-off-by: SeeuSim <[email protected]>
1 parent d40e61c commit 76e68bb

File tree

14 files changed

+307
-168
lines changed

14 files changed

+307
-168
lines changed

backend/user/src/lib/cookies/index.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,13 @@ export const generateCookie = <T extends object>(payload: T) => {
1010
};
1111

1212
export const isCookieValid = (cookie: string) => {
13-
return jwt.verify(cookie, JWT_SECRET_KEY, {
14-
ignoreExpiration: false,
15-
});
13+
try {
14+
return jwt.verify(cookie, JWT_SECRET_KEY, {
15+
ignoreExpiration: false,
16+
});
17+
} catch (error) {
18+
return false;
19+
}
1620
};
1721

1822
export type CookiePayload = {

frontend/src/components/blocks/authed-layout.tsx

Lines changed: 0 additions & 16 deletions
This file was deleted.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './main-layout';
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { Fragment, useCallback, useEffect, useState } from 'react';
2+
import { Link, Outlet } from 'react-router-dom';
3+
4+
import { getBreadCrumbs } from '@/lib/routes';
5+
import { cn } from '@/lib/utils';
6+
import { useRouterLocation } from '@/lib/hooks';
7+
import {
8+
Breadcrumb,
9+
BreadcrumbItem,
10+
BreadcrumbLink,
11+
BreadcrumbList,
12+
BreadcrumbSeparator,
13+
} from '@/components/ui/breadcrumb';
14+
import { BreadCrumb, BreadCrumbProvider } from '@/stores/breadcrumb-store';
15+
16+
export const AuthedLayout = () => {
17+
const [breadcrumbs, setCrumbs] = useState<Array<BreadCrumb>>([]);
18+
const {
19+
location: { pathname },
20+
} = useRouterLocation();
21+
22+
useEffect(() => {
23+
setCrumbs(getBreadCrumbs(pathname));
24+
}, [pathname]);
25+
const isLast = useCallback((index: number) => index === breadcrumbs.length - 1, [breadcrumbs]);
26+
27+
return (
28+
<div
29+
id='main'
30+
className={cn(
31+
'flex w-full flex-col overscroll-contain',
32+
'h-[calc(100dvh-64px)]' // The nav is 64px
33+
)}
34+
>
35+
<BreadCrumbProvider
36+
value={{
37+
breadcrumbs,
38+
setCrumbs,
39+
}}
40+
>
41+
{breadcrumbs.length > 0 && (
42+
<div className='bg-secondary/50 flex w-full p-4 px-6'>
43+
<Breadcrumb>
44+
<BreadcrumbList>
45+
{breadcrumbs.map(({ path, title }, index) => (
46+
<Fragment key={index}>
47+
<BreadcrumbItem>
48+
<BreadcrumbLink asChild>
49+
<Link
50+
to={path}
51+
className={cn(isLast(index) && 'text-secondary-foreground')}
52+
>
53+
{title}
54+
</Link>
55+
</BreadcrumbLink>
56+
</BreadcrumbItem>
57+
{!isLast(index) && <BreadcrumbSeparator />}
58+
</Fragment>
59+
))}
60+
</BreadcrumbList>
61+
</Breadcrumb>
62+
</div>
63+
)}
64+
</BreadCrumbProvider>
65+
<Outlet />
66+
</div>
67+
);
68+
};

frontend/src/components/blocks/nav-bar.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { HamburgerMenuIcon } from '@radix-ui/react-icons';
22
import { observer } from 'mobx-react';
3+
import { Link } from 'react-router-dom';
34

45
import { Logo } from '@/components/common/logo';
56
import { MobileThemeSwitch } from '@/components/common/mobile-theme-switch';
67
import { ThemeSwitch } from '@/components/common/theme-switch';
78
import { UserDropdown } from '@/components/common/user-dropdown';
8-
99
import { Button } from '@/components/ui/button';
10+
1011
import { useRouterLocation } from '@/lib/hooks';
1112
import { ROUTES } from '@/lib/routes';
1213

@@ -21,7 +22,9 @@ const NavBar = observer(() => {
2122
{!isUnauthedRoute && (
2223
<>
2324
<Button variant='ghost'>Start</Button>
24-
<Button variant='ghost'>Questions</Button>
25+
<Button variant='ghost' asChild>
26+
<Link to={ROUTES.QUESTIONS}>Questions</Link>
27+
</Button>
2528
</>
2629
)}
2730
<div className='ml-auto flex items-center gap-4 md:ml-auto md:gap-2 lg:gap-4'>

frontend/src/components/blocks/route-guard.tsx

Lines changed: 22 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import type { QueryClient } from '@tanstack/react-query';
2-
import { Suspense } from 'react';
2+
import { Suspense, useEffect, useState } from 'react';
33
import {
44
Await,
55
defer,
66
type LoaderFunctionArgs,
7-
Navigate,
87
Outlet,
98
useLoaderData,
9+
useNavigate,
1010
} from 'react-router-dom';
1111

1212
import { usePageTitle } from '@/lib/hooks/use-page-title';
13-
import { ROUTES } from '@/lib/routes';
13+
import { ROUTES, UNAUTHED_ROUTES } from '@/lib/routes';
1414
import { checkIsAuthed } from '@/services/user-service';
1515
import { Loading } from './loading';
1616

@@ -19,48 +19,43 @@ export const loader =
1919
async ({ request }: LoaderFunctionArgs) => {
2020
const route = new URL(request.url);
2121
const path = route.pathname;
22-
const unAuthedRoutes = [ROUTES.LOGIN, ROUTES.SIGNUP, ROUTES.FORGOT_PASSWORD];
23-
const unAuthedRoute = unAuthedRoutes.includes(path);
22+
const isUnauthedRoute = UNAUTHED_ROUTES.includes(path);
2423

2524
return defer({
26-
isAuthed: await queryClient.ensureQueryData({
25+
payload: await queryClient.ensureQueryData({
2726
queryKey: ['isAuthed'],
2827
queryFn: async () => {
29-
return await checkIsAuthed();
28+
return {
29+
authedPayload: await checkIsAuthed(),
30+
isAuthedRoute: !isUnauthedRoute,
31+
path,
32+
};
3033
},
3134
staleTime: ({ state: { data } }) => {
3235
const now = new Date();
33-
const expiresAt = data?.expiresAt ?? now;
36+
const expiresAt = data?.authedPayload?.expiresAt ?? now;
3437
return Math.max(expiresAt.getTime() - now.getTime(), 0);
3538
},
3639
}),
37-
authedRoute: !unAuthedRoute,
38-
path,
3940
});
4041
};
4142

4243
export const RouteGuard = () => {
4344
const data = useLoaderData() as Awaited<ReturnType<ReturnType<typeof loader>>>['data'];
45+
const navigate = useNavigate();
4446
return (
4547
<Suspense fallback={<Loading />}>
46-
<Await resolve={data}>
47-
{({ isAuthed, authedRoute, path }) => {
48+
<Await resolve={data.payload}>
49+
{({ authedPayload, isAuthedRoute, path }) => {
50+
const [isLoading, setIsLoading] = useState(true);
51+
useEffect(() => {
52+
if (authedPayload.isAuthed !== isAuthedRoute) {
53+
navigate(isAuthedRoute ? ROUTES.LOGIN : ROUTES.HOME);
54+
}
55+
setIsLoading(false);
56+
}, []);
4857
usePageTitle(path);
49-
return isAuthed.isAuthed ? (
50-
authedRoute ? (
51-
// Route is authed and user is authed - proceed
52-
<Outlet />
53-
) : (
54-
// Route is unauthed and user is authed - navigate to home
55-
<Navigate to='/' />
56-
)
57-
) : authedRoute ? (
58-
// Route is authed, but user is not - force login
59-
<Navigate to='/login' />
60-
) : (
61-
// Route is unauthed and user is not - proceed
62-
<Outlet />
63-
);
58+
return isLoading ? <Loading /> : <Outlet />;
6459
}}
6560
</Await>
6661
</Suspense>

frontend/src/lib/hooks/use-page-title.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,16 @@ export const usePageTitle = (path: string) => {
55
const isDocumentDefined = typeof document !== 'undefined';
66
const originalTitle = useRef(isDocumentDefined ? document.title : null);
77
useEffect(() => {
8-
if (!isDocumentDefined) return;
9-
if (document.title !== path) document.title = getPageTitle(path);
8+
if (!isDocumentDefined) {
9+
return;
10+
}
11+
if (document.title !== path) {
12+
document.title = getPageTitle(path);
13+
}
1014
return () => {
11-
if (originalTitle.current) document.title = originalTitle.current;
15+
if (originalTitle.current) {
16+
document.title = originalTitle.current;
17+
}
1218
};
1319
}, []);
1420
};

frontend/src/lib/hooks/use-router-location.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import { useMemo } from 'react';
22
import { useLocation } from 'react-router-dom';
33

4-
import { ROUTES } from '@/lib/routes';
4+
import { ROUTES, UNAUTHED_ROUTES } from '@/lib/routes';
55

66
export const useRouterLocation = () => {
77
const location = useLocation();
88

99
const data = useMemo(() => {
1010
const { pathname } = location;
1111
return {
12-
isUnauthedRoute: [ROUTES.LOGIN, ROUTES.SIGNUP, ROUTES.FORGOT_PASSWORD].includes(pathname),
12+
isUnauthedRoute: UNAUTHED_ROUTES.includes(pathname),
1313
isLogin: pathname === ROUTES.LOGIN,
1414
};
1515
}, [location]);

frontend/src/lib/router.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
import { createBrowserRouter } from 'react-router-dom';
22

3-
import { AuthedLayout } from '@/components/blocks/authed-layout';
3+
import { AuthedLayout } from '@/components/blocks/authed';
44
import { RootLayout } from '@/components/blocks/root-layout';
55
import { RouteGuard, loader as routeGuardLoader } from '@/components/blocks/route-guard';
66

77
import { ForgotPassword } from '@/routes/forgot-password';
88
import { Login } from '@/routes/login';
9-
import { loader as qnDetailsLoader, QuestionDetails } from '@/routes/questions/details';
9+
import { QuestionDetails, loader as questionDetailsLoader } from '@/routes/questions/details';
1010
import { SignUp } from '@/routes/signup';
1111

1212
import { queryClient } from './query-client';
1313
import { ROUTES } from './routes';
14+
import { QuestionsList } from '@/routes/questions/list';
1415

1516
export const router = createBrowserRouter([
1617
{
@@ -24,9 +25,13 @@ export const router = createBrowserRouter([
2425
path: ROUTES.HOME,
2526
element: <AuthedLayout />,
2627
children: [
28+
{
29+
path: ROUTES.QUESTIONS,
30+
element: <QuestionsList />,
31+
},
2732
{
2833
path: ROUTES.QUESTION_DETAILS,
29-
loader: qnDetailsLoader(queryClient),
34+
loader: questionDetailsLoader(queryClient),
3035
element: <QuestionDetails />,
3136
},
3237
],

frontend/src/lib/routes.ts

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,47 @@
1+
import type { BreadCrumb } from '@/stores/breadcrumb-store';
2+
13
export const ROUTES = {
24
HOME: '/',
35
LOGIN: '/login',
46
SIGNUP: '/signup',
57
FORGOT_PASSWORD: '/forgot-password',
6-
QUESTION_DETAILS: 'questions/:questionId',
8+
QUESTIONS: '/questions',
9+
QUESTION_DETAILS: '/questions/:questionId',
10+
};
11+
12+
const TOP_LEVEL_AUTHED_ROUTES = {
13+
[ROUTES.QUESTIONS]: [
14+
{
15+
path: ROUTES.QUESTIONS,
16+
title: 'Questions',
17+
},
18+
],
719
};
820

21+
export const getBreadCrumbs = (path: string): Array<BreadCrumb> => {
22+
for (const key of Object.keys(TOP_LEVEL_AUTHED_ROUTES)) {
23+
if (path.startsWith(key)) {
24+
return [
25+
{
26+
path: ROUTES.HOME,
27+
title: 'Home',
28+
},
29+
...TOP_LEVEL_AUTHED_ROUTES[key],
30+
];
31+
}
32+
}
33+
return [];
34+
};
35+
36+
export const UNAUTHED_ROUTES = [ROUTES.LOGIN, ROUTES.SIGNUP, ROUTES.FORGOT_PASSWORD];
37+
938
const TITLES: Record<string, string> = {
10-
HOME: '',
11-
LOGIN: 'Start Interviewing Today',
12-
SIGNUP: 'Create an Account',
13-
FORGOT_PASSWORD: 'Forgot Password',
39+
[ROUTES.LOGIN]: 'Start Interviewing Today',
40+
[ROUTES.SIGNUP]: 'Create an Account',
41+
[ROUTES.FORGOT_PASSWORD]: 'Forgot Password',
1442
};
1543

1644
export const getPageTitle = (path: string) => {
1745
const title = TITLES[path];
18-
return title !== undefined
19-
? ['Peerprep', title].filter((v) => v.length > 0).join(' - ')
20-
: 'Peerprep';
46+
return title ?? 'Peerprep';
2147
};

0 commit comments

Comments
 (0)