Skip to content

Commit f0184bf

Browse files
authored
Merge pull request #19 from CS3219-AY2425S1/chore/tidy-questions-layout-code
Chore/tidy questions layout code
2 parents d40e61c + c2ff542 commit f0184bf

File tree

16 files changed

+275
-117
lines changed

16 files changed

+275
-117
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: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Outlet } from 'react-router-dom';
2+
3+
import { cn } from '@/lib/utils';
4+
5+
export const AuthedLayout = () => {
6+
return (
7+
<div
8+
id='main'
9+
className={cn(
10+
'flex w-full flex-col overscroll-contain',
11+
'h-[calc(100dvh-64px)]' // The nav is 64px
12+
)}
13+
>
14+
<Outlet />
15+
</div>
16+
);
17+
};
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import {
2+
Breadcrumb,
3+
BreadcrumbItem,
4+
BreadcrumbLink,
5+
BreadcrumbList,
6+
BreadcrumbSeparator,
7+
} from '@/components/ui/breadcrumb';
8+
import { cn } from '@/lib/utils';
9+
import { BreadCrumb } from '@/stores/breadcrumb-store';
10+
import { FC, Fragment, PropsWithChildren } from 'react';
11+
import { Link } from 'react-router-dom';
12+
13+
type IBreadCrumbBannerProps = {
14+
crumbs: Array<BreadCrumb>;
15+
};
16+
17+
export const WithNavBanner: FC<PropsWithChildren<IBreadCrumbBannerProps>> = ({
18+
children,
19+
crumbs,
20+
}) => {
21+
const isLast = (index: number) => index === crumbs.length - 1;
22+
return (
23+
<>
24+
<div className='bg-secondary/50 flex w-full p-4 px-6'>
25+
<Breadcrumb>
26+
<BreadcrumbList>
27+
{crumbs.map(({ path, title }, index) => (
28+
<Fragment key={index}>
29+
<BreadcrumbItem>
30+
<BreadcrumbLink asChild>
31+
<Link to={path} className={cn(isLast(index) && 'text-secondary-foreground')}>
32+
{title}
33+
</Link>
34+
</BreadcrumbLink>
35+
</BreadcrumbItem>
36+
{!isLast(index) && <BreadcrumbSeparator />}
37+
</Fragment>
38+
))}
39+
</BreadcrumbList>
40+
</Breadcrumb>
41+
</div>
42+
{children}
43+
</>
44+
);
45+
};

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>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { BreadCrumb } from '@/stores/breadcrumb-store';
2+
import { getBreadCrumbs } from '../routes';
3+
import { useRouterLocation } from './use-router-location';
4+
5+
export const useCrumbs = (...extraCrumbs: Array<BreadCrumb>) => {
6+
const {
7+
location: { pathname },
8+
} = useRouterLocation();
9+
const crumbs = getBreadCrumbs(pathname);
10+
return {
11+
crumbs: [...crumbs, ...extraCrumbs].map((v) => ({
12+
...v,
13+
path: v.path.replace('<CURRENT>', pathname),
14+
})),
15+
};
16+
};

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

0 commit comments

Comments
 (0)