Skip to content

Commit 74ff903

Browse files
authored
Next app router improvements (#222)
* next app router improvements * upgrade vite
1 parent 9010f0d commit 74ff903

File tree

43 files changed

+1260
-968
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1260
-968
lines changed

apps/nextjs-app/e2e/tests/auth.setup.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ setup('authenticate', async ({ page }) => {
2828
// log out:
2929
await page.getByRole('button', { name: 'Open user menu' }).click();
3030
await page.getByRole('menuitem', { name: 'Sign Out' }).click();
31-
await page.waitForURL('/auth/login?redirectTo=%252Fapp');
31+
await page.waitForURL('/auth/login?redirectTo=%2Fapp');
3232

3333
// log in:
3434
await page.getByLabel('Email Address').click();

apps/nextjs-app/mock-server.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,19 @@ app.use(
1616
);
1717

1818
app.use(express.json());
19-
app.use(logger({ level: 'silent' }));
19+
app.use(
20+
logger({
21+
level: 'info',
22+
redact: ['req.headers', 'res.headers'],
23+
transport: {
24+
target: 'pino-pretty',
25+
options: {
26+
colorize: true,
27+
translateTime: true,
28+
},
29+
},
30+
}),
31+
);
2032
app.use(createMiddleware(...handlers));
2133

2234
initializeDb().then(() => {

apps/nextjs-app/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@
4242
"react-dom": "^18.3.1",
4343
"react-error-boundary": "^4.0.13",
4444
"react-hook-form": "^7.51.3",
45-
"react-query-auth": "^2.3.0",
4645
"tailwind-merge": "^2.3.0",
4746
"tailwindcss-animate": "^1.0.7",
4847
"zod": "^3.23.4",
@@ -109,7 +108,7 @@
109108
"tsx": "^4.17.0",
110109
"typescript": "^5.4.5",
111110
"vite-tsconfig-paths": "^4.3.2",
112-
"vitest": "^1.5.2"
111+
"vitest": "^2.1.4"
113112
},
114113
"msw": {
115114
"workerDirectory": "public"
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
'use client';
2+
3+
import { useUser } from '@/lib/auth';
4+
5+
export const DashboardInfo = () => {
6+
const user = useUser();
7+
8+
return (
9+
<>
10+
<h1 className="text-xl">
11+
Welcome <b>{`${user.data?.firstName} ${user.data?.lastName}`}</b>
12+
</h1>
13+
<h4 className="my-3">
14+
Your role is : <b>{user.data?.role}</b>
15+
</h4>
16+
<p className="font-medium">In this application you can:</p>
17+
{user.data?.role === 'USER' && (
18+
<ul className="my-4 list-inside list-disc">
19+
<li>Create comments in discussions</li>
20+
<li>Delete own comments</li>
21+
</ul>
22+
)}
23+
{user.data?.role === 'ADMIN' && (
24+
<ul className="my-4 list-inside list-disc">
25+
<li>Create discussions</li>
26+
<li>Edit discussions</li>
27+
<li>Delete discussions</li>
28+
<li>Comment on discussions</li>
29+
<li>Delete all comments</li>
30+
</ul>
31+
)}
32+
</>
33+
);
34+
};

apps/nextjs-app/src/components/layouts/dashboard-layout.tsx renamed to apps/nextjs-app/src/app/app/_components/dashboard-layout.tsx

Lines changed: 18 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,21 @@
33
import { Home, PanelLeft, Folder, Users, User2 } from 'lucide-react';
44
import NextLink from 'next/link';
55
import { useRouter, usePathname } from 'next/navigation';
6-
import { Suspense } from 'react';
76
import { ErrorBoundary } from 'react-error-boundary';
87

98
import { Button } from '@/components/ui/button';
109
import { Drawer, DrawerContent, DrawerTrigger } from '@/components/ui/drawer';
11-
import { Spinner } from '@/components/ui/spinner';
12-
import { paths } from '@/config/paths';
13-
import { AuthLoader, useLogout } from '@/lib/auth';
14-
import { ROLES, useAuthorization } from '@/lib/authorization';
15-
import { cn } from '@/utils/cn';
16-
1710
import {
1811
DropdownMenu,
1912
DropdownMenuContent,
2013
DropdownMenuItem,
2114
DropdownMenuSeparator,
2215
DropdownMenuTrigger,
23-
} from '../ui/dropdown';
24-
import { Link } from '../ui/link';
16+
} from '@/components/ui/dropdown';
17+
import { Link } from '@/components/ui/link';
18+
import { paths } from '@/config/paths';
19+
import { useLogout, useUser } from '@/lib/auth';
20+
import { cn } from '@/utils/cn';
2521

2622
type SideNavigationItem = {
2723
name: string;
@@ -41,14 +37,16 @@ const Logo = () => {
4137
};
4238

4339
const Layout = ({ children }: { children: React.ReactNode }) => {
44-
const logout = useLogout();
45-
const { checkAccess } = useAuthorization();
40+
const user = useUser();
4641
const pathname = usePathname();
4742
const router = useRouter();
43+
const logout = useLogout({
44+
onSuccess: () => router.push(paths.auth.login.getHref(pathname)),
45+
});
4846
const navigation = [
4947
{ name: 'Dashboard', to: paths.app.root.getHref(), icon: Home },
5048
{ name: 'Discussions', to: paths.app.discussions.getHref(), icon: Folder },
51-
checkAccess({ allowedRoles: [ROLES.ADMIN] }) && {
49+
user.data?.role === 'ADMIN' && {
5250
name: 'Users',
5351
to: paths.app.users.getHref(),
5452
icon: Users,
@@ -152,7 +150,7 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
152150
<DropdownMenuSeparator />
153151
<DropdownMenuItem
154152
className={cn('block px-4 py-2 text-sm text-gray-700 w-full')}
155-
onClick={() => logout.mutate({})}
153+
onClick={() => logout.mutate()}
156154
>
157155
Sign Out
158156
</DropdownMenuItem>
@@ -167,6 +165,10 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
167165
);
168166
};
169167

168+
function Fallback({ error }: { error: Error }) {
169+
return <p>Error: {error.message ?? 'Something went wrong!'}</p>;
170+
}
171+
170172
export const DashboardLayout = ({
171173
children,
172174
}: {
@@ -175,28 +177,9 @@ export const DashboardLayout = ({
175177
const pathname = usePathname();
176178
return (
177179
<Layout>
178-
<Suspense
179-
fallback={
180-
<div className="flex size-full items-center justify-center">
181-
<Spinner size="xl" />
182-
</div>
183-
}
184-
>
185-
<ErrorBoundary
186-
key={pathname}
187-
fallback={<div>Something went wrong!</div>}
188-
>
189-
<AuthLoader
190-
renderLoading={() => (
191-
<div className="flex size-full items-center justify-center">
192-
<Spinner size="xl" />
193-
</div>
194-
)}
195-
>
196-
{children}
197-
</AuthLoader>
198-
</ErrorBoundary>
199-
</Suspense>
180+
<ErrorBoundary key={pathname} FallbackComponent={Fallback}>
181+
{children}
182+
</ErrorBoundary>
200183
</Layout>
201184
);
202185
};

apps/nextjs-app/src/app/app/discussions/[discussionId]/__tests__/discussion.test.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
waitForLoadingToFinish,
1212
} from '@/testing/test-utils';
1313

14-
import DiscussionPage from '../page';
14+
import { Discussion } from '../_components/discussion';
1515

1616
vi.mock('next/navigation', async () => {
1717
const actual = await vi.importActual('next/navigation');
@@ -33,11 +33,14 @@ const renderDiscussion = async () => {
3333

3434
vi.mocked(useParams).mockReturnValue({ discussionId: fakeDiscussion.id });
3535

36-
const utils = await renderApp(<DiscussionPage />, {
37-
user: fakeUser,
38-
path: `/app/discussions/:discussionId`,
39-
url: `/app/discussions/${fakeDiscussion.id}`,
40-
});
36+
const utils = await renderApp(
37+
<Discussion discussionId={fakeDiscussion.id} />,
38+
{
39+
user: fakeUser,
40+
path: `/app/discussions/:discussionId`,
41+
url: `/app/discussions/${fakeDiscussion.id}`,
42+
},
43+
);
4144

4245
await waitForLoadingToFinish();
4346

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
'use client';
2+
3+
import { ErrorBoundary } from 'react-error-boundary';
4+
5+
import { ContentLayout } from '@/components/layouts/content-layout';
6+
import { Comments } from '@/features/comments/components/comments';
7+
import { useDiscussion } from '@/features/discussions/api/get-discussion';
8+
import { DiscussionView } from '@/features/discussions/components/discussion-view';
9+
10+
export const Discussion = ({ discussionId }: { discussionId: string }) => {
11+
const discussion = useDiscussion({ discussionId });
12+
13+
return (
14+
<ContentLayout title={discussion?.data?.data?.title}>
15+
<DiscussionView discussionId={discussionId} />
16+
<div className="mt-8">
17+
<ErrorBoundary
18+
fallback={
19+
<div>Failed to load comments. Try to refresh the page.</div>
20+
}
21+
>
22+
<Comments discussionId={discussionId} />
23+
</ErrorBoundary>
24+
</div>
25+
</ContentLayout>
26+
);
27+
};

apps/nextjs-app/src/app/app/discussions/[discussionId]/page.tsx

Lines changed: 59 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,71 @@
1-
'use client';
1+
import {
2+
dehydrate,
3+
HydrationBoundary,
4+
QueryClient,
5+
} from '@tanstack/react-query';
26

3-
import { useParams } from 'next/navigation';
4-
import { ErrorBoundary } from 'react-error-boundary';
7+
import { getInfiniteCommentsQueryOptions } from '@/features/comments/api/get-comments';
8+
import {
9+
getDiscussion,
10+
getDiscussionQueryOptions,
11+
} from '@/features/discussions/api/get-discussion';
512

6-
import { ContentLayout } from '@/components/layouts/content-layout';
7-
import { Spinner } from '@/components/ui/spinner';
8-
import { Comments } from '@/features/comments/components/comments';
9-
import { useDiscussion } from '@/features/discussions/api/get-discussion';
10-
import { DiscussionView } from '@/features/discussions/components/discussion-view';
13+
import { Discussion } from './_components/discussion';
1114

12-
const DiscussionPage = () => {
13-
const params = useParams();
14-
const discussionId = params?.discussionId as string;
15+
export const generateMetadata = async ({
16+
params,
17+
}: {
18+
params: Promise<{ discussionId: string }>;
19+
}) => {
20+
const discussionId = (await params).discussionId;
1521

16-
const discussionQuery = useDiscussion({
17-
discussionId,
18-
});
22+
const discussion = await getDiscussion({ discussionId });
1923

20-
if (discussionQuery.isLoading) {
21-
return (
22-
<div className="flex h-48 w-full items-center justify-center">
23-
<Spinner size="lg" />
24-
</div>
25-
);
26-
}
24+
return {
25+
title: discussion.data?.title,
26+
description: discussion.data?.title,
27+
};
28+
};
29+
30+
const preloadData = async (discussionId: string) => {
31+
const queryClient = new QueryClient();
32+
33+
await Promise.all([
34+
queryClient.prefetchQuery(getDiscussionQueryOptions(discussionId)),
35+
queryClient.prefetchInfiniteQuery(
36+
getInfiniteCommentsQueryOptions(discussionId),
37+
),
38+
]);
2739

28-
const discussion = discussionQuery.data?.data;
40+
const dehydratedState = dehydrate(queryClient);
41+
42+
return {
43+
dehydratedState,
44+
queryClient,
45+
};
46+
};
47+
48+
const DiscussionPage = async ({
49+
params,
50+
}: {
51+
params: Promise<{
52+
discussionId: string;
53+
}>;
54+
}) => {
55+
const discussionId = (await params).discussionId;
56+
57+
const { dehydratedState, queryClient } = await preloadData(discussionId);
58+
59+
const discussion = queryClient.getQueryData(
60+
getDiscussionQueryOptions(discussionId).queryKey,
61+
);
2962

30-
if (!discussion) return null;
63+
if (!discussion?.data) return <div>Discussion not found</div>;
3164

3265
return (
33-
<ContentLayout title={discussion.title}>
34-
<DiscussionView discussionId={discussionId} />
35-
<div className="mt-8">
36-
<ErrorBoundary
37-
fallback={
38-
<div>Failed to load comments. Try to refresh the page.</div>
39-
}
40-
>
41-
<Comments discussionId={discussionId} />
42-
</ErrorBoundary>
43-
</div>
44-
</ContentLayout>
66+
<HydrationBoundary state={dehydratedState}>
67+
<Discussion discussionId={discussionId} />
68+
</HydrationBoundary>
4569
);
4670
};
4771

apps/nextjs-app/src/app/app/discussions/__tests__/discussions.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
} from '@/testing/test-utils';
1212
import { formatDate } from '@/utils/format';
1313

14-
import DiscussionsPage from '../page';
14+
import { Discussions } from '../_components/discussions';
1515

1616
beforeAll(() => {
1717
vi.spyOn(console, 'error').mockImplementation(() => {});
@@ -25,7 +25,7 @@ test(
2525
'should create, render and delete discussions',
2626
{ timeout: 10000 },
2727
async () => {
28-
await renderApp(<DiscussionsPage />);
28+
await renderApp(<Discussions />);
2929

3030
await waitForLoadingToFinish();
3131

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
'use client';
2+
3+
import { useQueryClient } from '@tanstack/react-query';
4+
5+
import { ContentLayout } from '@/components/layouts/content-layout';
6+
import { getInfiniteCommentsQueryOptions } from '@/features/comments/api/get-comments';
7+
import { CreateDiscussion } from '@/features/discussions/components/create-discussion';
8+
import { DiscussionsList } from '@/features/discussions/components/discussions-list';
9+
10+
export const Discussions = () => {
11+
const queryClient = useQueryClient();
12+
13+
return (
14+
<ContentLayout title="Discussions">
15+
<div className="flex justify-end">
16+
<CreateDiscussion />
17+
</div>
18+
<div className="mt-4">
19+
<DiscussionsList
20+
onDiscussionPrefetch={(id) => {
21+
// Prefetch the comments data when the user hovers over the link in the list
22+
queryClient.prefetchInfiniteQuery(
23+
getInfiniteCommentsQueryOptions(id),
24+
);
25+
}}
26+
/>
27+
</div>
28+
</ContentLayout>
29+
);
30+
};

0 commit comments

Comments
 (0)