Skip to content

Commit 419c5b4

Browse files
authored
Merge pull request #8 from nitic-pbl-p8/7/add-header-for-auth-guarded-page
2 parents 5f95be7 + 3fced9a commit 419c5b4

File tree

19 files changed

+401
-66
lines changed

19 files changed

+401
-66
lines changed

apps/website/next.config.mjs

-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
/** @type {import('next').NextConfig} */
22
const config = {
3-
experimental: {
4-
typedRoutes: true,
5-
},
63
images: {
74
remotePatterns: [
85
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs';
2+
import type { NextPage } from 'next';
3+
import { cookies } from 'next/headers';
4+
import { InAppHeaderRouteIndicatorDivider, InAppHeaderRouteIndicatorIcon, InAppHeaderRouteIndicatorLabel } from '#website/layout/global/header';
5+
import { findUserUseCase } from '#website/use-case/find-user';
6+
import { findUserLostItemsUseCase } from '~website/src/use-case/find-user-lost-items';
7+
8+
const RouteIndicator: NextPage = async () => {
9+
const cookieStore = cookies();
10+
11+
const supabase = createServerComponentClient({ cookies: () => cookieStore });
12+
const {
13+
data: { user },
14+
} = await supabase.auth.getUser();
15+
if (!user) {
16+
return null;
17+
}
18+
19+
const foundUser = await findUserUseCase(user.id);
20+
if (!foundUser) {
21+
return null;
22+
}
23+
24+
const userLostItem = await findUserLostItemsUseCase(foundUser.authId);
25+
if (!userLostItem) {
26+
return null;
27+
}
28+
29+
return userLostItem.currentTargetLostItem && foundUser.lostAndFoundState !== 'NONE' ? (
30+
<>
31+
<InAppHeaderRouteIndicatorDivider />
32+
{!!userLostItem.currentTargetLostItem.lostItem.imageUrls[0] && (
33+
<InAppHeaderRouteIndicatorIcon
34+
src={userLostItem.currentTargetLostItem.lostItem.imageUrls[0]}
35+
alt={`An image of ${userLostItem.currentTargetLostItem.lostItem.title}`}
36+
/>
37+
)}
38+
<InAppHeaderRouteIndicatorLabel>{userLostItem.currentTargetLostItem.lostItem.title}</InAppHeaderRouteIndicatorLabel>
39+
</>
40+
) : (
41+
<>
42+
<InAppHeaderRouteIndicatorDivider />
43+
<InAppHeaderRouteIndicatorLabel>Overview</InAppHeaderRouteIndicatorLabel>
44+
</>
45+
);
46+
};
47+
48+
export default RouteIndicator;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import type { ReactNode } from 'react';
2+
3+
const RouteIndicatorPage = (): ReactNode => null;
4+
5+
export default RouteIndicatorPage;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import type { ReactNode } from 'react';
2+
3+
const RouteIndicatorPage = (): ReactNode => null;
4+
5+
export default RouteIndicatorPage;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import type { ReactNode } from 'react';
2+
3+
const RouteIndicatorPage = (): ReactNode => null;
4+
5+
export default RouteIndicatorPage;
+25-6
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,35 @@
1+
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs';
12
import type { NextPage } from 'next';
3+
import { cookies } from 'next/headers';
24
import type { ReactNode } from 'react';
5+
import { InAppHeader } from '#website/layout/global/header';
36
import { AuthGuardProvider } from '#website/layout/with-auth/auth-guard-provider';
7+
import { findUserUseCase } from '#website/use-case/find-user';
48

59
type WithAuthLayoutProps = {
610
children: ReactNode;
11+
routeIndicator: ReactNode;
712
};
813

9-
const WithAuthLayout: NextPage<WithAuthLayoutProps> = ({ children }) => (
10-
<>
11-
<AuthGuardProvider />
12-
{children}
13-
</>
14-
);
14+
const WithAuthLayout: NextPage<WithAuthLayoutProps> = async ({ children, routeIndicator }) => {
15+
// HACK: To avoid next build errors, functions that depend on async contexts need to be called outside the function that creates the new execution context.
16+
// ref: https://nextjs.org/docs/messages/dynamic-server-error
17+
const cookieStore = cookies();
18+
19+
const supabase = createServerComponentClient({ cookies: () => cookieStore });
20+
const {
21+
data: { user },
22+
} = await supabase.auth.getUser();
23+
24+
const foundUser = user && (await findUserUseCase(user.id));
25+
26+
return (
27+
<>
28+
<InAppHeader user={foundUser}>{routeIndicator}</InAppHeader>
29+
<AuthGuardProvider />
30+
{children}
31+
</>
32+
);
33+
};
1534

1635
export default WithAuthLayout;
File renamed without changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs';
2+
import type { NextPage } from 'next';
3+
import { cookies } from 'next/headers';
4+
import type { ReactNode } from 'react';
5+
import { Header } from '#website/layout/global/header';
6+
import { findUserUseCase } from '#website/use-case/find-user';
7+
8+
type WithNoAuthLayoutProps = {
9+
children: ReactNode;
10+
};
11+
12+
const WithNoAuthLayout: NextPage<WithNoAuthLayoutProps> = async ({ children }) => {
13+
// HACK: To avoid next build errors, functions that depend on async contexts need to be called outside the function that creates the new execution context.
14+
// ref: https://nextjs.org/docs/messages/dynamic-server-error
15+
const cookieStore = cookies();
16+
17+
const supabase = createServerComponentClient({ cookies: () => cookieStore });
18+
const {
19+
data: { user },
20+
} = await supabase.auth.getUser();
21+
22+
const foundUser = user && (await findUserUseCase(user.id));
23+
24+
return (
25+
<>
26+
<Header user={foundUser} />
27+
{children}
28+
</>
29+
);
30+
};
31+
32+
export default WithNoAuthLayout;
File renamed without changes.

apps/website/src/app/layout.tsx

+21-39
Original file line numberDiff line numberDiff line change
@@ -4,55 +4,37 @@ import { firaCode, getFontVariables, notoSans } from '@lockerai/core/font/family
44
import { getBaseUrl } from '@lockerai/core/util/get-base-url';
55
import { colors } from '@lockerai/design-token';
66
import { cn } from '@lockerai/tailwind';
7-
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs';
87
import type { Metadata, NextPage, Viewport } from 'next';
9-
import { cookies } from 'next/headers';
108
import type { ReactNode } from 'react';
119
import { UrqlProvider } from '#website/infra/urql/ssr';
1210
import { Footer } from '#website/layout/global/footer';
13-
import { Header } from '#website/layout/global/header';
14-
import { findUserUseCase } from '#website/use-case/find-user';
1511
import '#website/style/global.css';
1612

1713
type RootLayoutProps = {
1814
children: ReactNode;
1915
};
2016

21-
const RootLayout: NextPage<RootLayoutProps> = async ({ children }) => {
22-
// HACK: To avoid next build errors, functions that depend on async contexts need to be called outside the function that creates the new execution context.
23-
// ref: https://nextjs.org/docs/messages/dynamic-server-error
24-
const cookieStore = cookies();
25-
26-
const supabase = createServerComponentClient({ cookies: () => cookieStore });
27-
const {
28-
data: { user },
29-
} = await supabase.auth.getUser();
30-
31-
const foundUser = user && (await findUserUseCase(user.id));
32-
33-
return (
34-
<html lang="en" suppressHydrationWarning>
35-
<head />
36-
<body className={cn(getFontVariables([firaCode, notoSans]), 'relative bg-sage-1 font-sans')}>
37-
<div
38-
aria-hidden
39-
className={cn(
40-
'absolute -z-20 h-full w-full bg-grid-light-green-7/50 dark:bg-grid-dark-green-7/50',
41-
'from-pure to-[70%] [-webkit-mask-image:linear-gradient(to_bottom,var(--tw-gradient-stops))] [mask-image:linear-gradient(to_bottom,var(--tw-gradient-stops))]',
42-
)}
43-
/>
44-
<UrqlProvider>
45-
<ThemeProvider attribute="data-theme" enableSystem defaultTheme="system">
46-
<Header user={foundUser} />
47-
{children}
48-
<Footer />
49-
<Sonner />
50-
</ThemeProvider>
51-
</UrqlProvider>
52-
</body>
53-
</html>
54-
);
55-
};
17+
const RootLayout: NextPage<RootLayoutProps> = async ({ children }) => (
18+
<html lang="en" suppressHydrationWarning>
19+
<head />
20+
<body className={cn(getFontVariables([firaCode, notoSans]), 'relative bg-sage-1 font-sans')}>
21+
<div
22+
aria-hidden
23+
className={cn(
24+
'absolute -z-20 h-full w-full bg-grid-light-green-7/50 dark:bg-grid-dark-green-7/50',
25+
'from-pure to-[70%] [-webkit-mask-image:linear-gradient(to_bottom,var(--tw-gradient-stops))] [mask-image:linear-gradient(to_bottom,var(--tw-gradient-stops))]',
26+
)}
27+
/>
28+
<UrqlProvider>
29+
<ThemeProvider attribute="data-theme" enableSystem defaultTheme="system">
30+
{children}
31+
<Footer />
32+
<Sonner />
33+
</ThemeProvider>
34+
</UrqlProvider>
35+
</body>
36+
</html>
37+
);
5638

5739
export default RootLayout;
5840

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { Meta, StoryObj } from '@storybook/react';
2+
import { HeaderLink } from './header-link';
3+
4+
type Story = StoryObj<typeof HeaderLink>;
5+
6+
const meta = {
7+
component: HeaderLink,
8+
args: {
9+
href: '/dashboard',
10+
children: 'Overview',
11+
},
12+
} satisfies Meta<typeof HeaderLink>;
13+
14+
export default meta;
15+
16+
export const Default: Story = {};
17+
18+
export const Selected: Story = {
19+
parameters: {
20+
nextjs: {
21+
appDirectory: true,
22+
navigation: {
23+
pathname: '/dashboard',
24+
},
25+
},
26+
},
27+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { Link } from '#core/component/link';
2+
import { type VariantProps, cn, tv } from '@lockerai/tailwind';
3+
import { usePathname } from 'next/navigation';
4+
import type { ComponentPropsWithoutRef, ReactNode } from 'react';
5+
import { useMemo } from 'react';
6+
7+
const headerLinkVariant = tv({
8+
base: 'flex items-center justify-center rounded-t-lg p-3 font-bold hover:bg-sage-3',
9+
variants: {
10+
selected: {
11+
true: 'border-b-2 border-sage-12 text-sage-12',
12+
false: 'text-sage-11',
13+
},
14+
},
15+
defaultVariants: {
16+
selected: false,
17+
},
18+
});
19+
20+
type HeaderLinkProps = ComponentPropsWithoutRef<typeof Link> & VariantProps<typeof headerLinkVariant>;
21+
22+
export const HeaderLink = ({ children, className, ...props }: HeaderLinkProps): ReactNode => {
23+
// Retrieve the current path starting with /.
24+
// Refer: https://nextjs.org/docs/app/api-reference/functions/use-pathname
25+
const currentPath = usePathname(); // e.g. `/docs/works/shelfree`
26+
// Check if the current path is the same as the href.
27+
const isBeingOpened = useMemo(() => !!props.href && currentPath === props.href.toString(), [currentPath, props.href]);
28+
29+
return (
30+
<Link className={cn(headerLinkVariant({ selected: isBeingOpened }), className)} {...props}>
31+
{children}
32+
</Link>
33+
);
34+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { HeaderLink } from './header-link';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
'use client';
2+
3+
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs';
4+
import { type ComponentPropsWithoutRef, type ReactNode, useEffect, useState } from 'react';
5+
import { InAppHeader as InAppHeaderPresenter } from './in-app-header.presenter';
6+
7+
export type HeaderProps = Omit<ComponentPropsWithoutRef<typeof InAppHeaderPresenter>, 'className'>;
8+
9+
export const InAppHeader = ({ user: initialUser, ...props }: HeaderProps): ReactNode => {
10+
const [user, setUser] = useState(initialUser);
11+
12+
const supabase = createClientComponentClient();
13+
14+
useEffect(() => {
15+
const {
16+
data: { subscription },
17+
} = supabase.auth.onAuthStateChange(async (_, session) => {
18+
if (!session) {
19+
setUser(null);
20+
}
21+
});
22+
23+
return () => {
24+
subscription.unsubscribe();
25+
};
26+
}, [supabase.auth]);
27+
28+
return <InAppHeaderPresenter user={user} {...props} />;
29+
};

0 commit comments

Comments
 (0)