Skip to content

Commit

Permalink
Next app router improvements (#222)
Browse files Browse the repository at this point in the history
* next app router improvements

* upgrade vite
  • Loading branch information
alan2207 authored Nov 12, 2024
1 parent 9010f0d commit 74ff903
Show file tree
Hide file tree
Showing 43 changed files with 1,260 additions and 968 deletions.
2 changes: 1 addition & 1 deletion apps/nextjs-app/e2e/tests/auth.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ setup('authenticate', async ({ page }) => {
// log out:
await page.getByRole('button', { name: 'Open user menu' }).click();
await page.getByRole('menuitem', { name: 'Sign Out' }).click();
await page.waitForURL('/auth/login?redirectTo=%252Fapp');
await page.waitForURL('/auth/login?redirectTo=%2Fapp');

// log in:
await page.getByLabel('Email Address').click();
Expand Down
14 changes: 13 additions & 1 deletion apps/nextjs-app/mock-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,19 @@ app.use(
);

app.use(express.json());
app.use(logger({ level: 'silent' }));
app.use(
logger({
level: 'info',
redact: ['req.headers', 'res.headers'],
transport: {
target: 'pino-pretty',
options: {
colorize: true,
translateTime: true,
},
},
}),
);
app.use(createMiddleware(...handlers));

initializeDb().then(() => {
Expand Down
3 changes: 1 addition & 2 deletions apps/nextjs-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@
"react-dom": "^18.3.1",
"react-error-boundary": "^4.0.13",
"react-hook-form": "^7.51.3",
"react-query-auth": "^2.3.0",
"tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7",
"zod": "^3.23.4",
Expand Down Expand Up @@ -109,7 +108,7 @@
"tsx": "^4.17.0",
"typescript": "^5.4.5",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.5.2"
"vitest": "^2.1.4"
},
"msw": {
"workerDirectory": "public"
Expand Down
34 changes: 34 additions & 0 deletions apps/nextjs-app/src/app/app/_components/dashboard-info.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use client';

import { useUser } from '@/lib/auth';

export const DashboardInfo = () => {
const user = useUser();

return (
<>
<h1 className="text-xl">
Welcome <b>{`${user.data?.firstName} ${user.data?.lastName}`}</b>
</h1>
<h4 className="my-3">
Your role is : <b>{user.data?.role}</b>
</h4>
<p className="font-medium">In this application you can:</p>
{user.data?.role === 'USER' && (
<ul className="my-4 list-inside list-disc">
<li>Create comments in discussions</li>
<li>Delete own comments</li>
</ul>
)}
{user.data?.role === 'ADMIN' && (
<ul className="my-4 list-inside list-disc">
<li>Create discussions</li>
<li>Edit discussions</li>
<li>Delete discussions</li>
<li>Comment on discussions</li>
<li>Delete all comments</li>
</ul>
)}
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,21 @@
import { Home, PanelLeft, Folder, Users, User2 } from 'lucide-react';
import NextLink from 'next/link';
import { useRouter, usePathname } from 'next/navigation';
import { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';

import { Button } from '@/components/ui/button';
import { Drawer, DrawerContent, DrawerTrigger } from '@/components/ui/drawer';
import { Spinner } from '@/components/ui/spinner';
import { paths } from '@/config/paths';
import { AuthLoader, useLogout } from '@/lib/auth';
import { ROLES, useAuthorization } from '@/lib/authorization';
import { cn } from '@/utils/cn';

import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '../ui/dropdown';
import { Link } from '../ui/link';
} from '@/components/ui/dropdown';
import { Link } from '@/components/ui/link';
import { paths } from '@/config/paths';
import { useLogout, useUser } from '@/lib/auth';
import { cn } from '@/utils/cn';

type SideNavigationItem = {
name: string;
Expand All @@ -41,14 +37,16 @@ const Logo = () => {
};

const Layout = ({ children }: { children: React.ReactNode }) => {
const logout = useLogout();
const { checkAccess } = useAuthorization();
const user = useUser();
const pathname = usePathname();
const router = useRouter();
const logout = useLogout({
onSuccess: () => router.push(paths.auth.login.getHref(pathname)),
});
const navigation = [
{ name: 'Dashboard', to: paths.app.root.getHref(), icon: Home },
{ name: 'Discussions', to: paths.app.discussions.getHref(), icon: Folder },
checkAccess({ allowedRoles: [ROLES.ADMIN] }) && {
user.data?.role === 'ADMIN' && {
name: 'Users',
to: paths.app.users.getHref(),
icon: Users,
Expand Down Expand Up @@ -152,7 +150,7 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
<DropdownMenuSeparator />
<DropdownMenuItem
className={cn('block px-4 py-2 text-sm text-gray-700 w-full')}
onClick={() => logout.mutate({})}
onClick={() => logout.mutate()}
>
Sign Out
</DropdownMenuItem>
Expand All @@ -167,6 +165,10 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
);
};

function Fallback({ error }: { error: Error }) {
return <p>Error: {error.message ?? 'Something went wrong!'}</p>;
}

export const DashboardLayout = ({
children,
}: {
Expand All @@ -175,28 +177,9 @@ export const DashboardLayout = ({
const pathname = usePathname();
return (
<Layout>
<Suspense
fallback={
<div className="flex size-full items-center justify-center">
<Spinner size="xl" />
</div>
}
>
<ErrorBoundary
key={pathname}
fallback={<div>Something went wrong!</div>}
>
<AuthLoader
renderLoading={() => (
<div className="flex size-full items-center justify-center">
<Spinner size="xl" />
</div>
)}
>
{children}
</AuthLoader>
</ErrorBoundary>
</Suspense>
<ErrorBoundary key={pathname} FallbackComponent={Fallback}>
{children}
</ErrorBoundary>
</Layout>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
waitForLoadingToFinish,
} from '@/testing/test-utils';

import DiscussionPage from '../page';
import { Discussion } from '../_components/discussion';

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

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

const utils = await renderApp(<DiscussionPage />, {
user: fakeUser,
path: `/app/discussions/:discussionId`,
url: `/app/discussions/${fakeDiscussion.id}`,
});
const utils = await renderApp(
<Discussion discussionId={fakeDiscussion.id} />,
{
user: fakeUser,
path: `/app/discussions/:discussionId`,
url: `/app/discussions/${fakeDiscussion.id}`,
},
);

await waitForLoadingToFinish();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'use client';

import { ErrorBoundary } from 'react-error-boundary';

import { ContentLayout } from '@/components/layouts/content-layout';
import { Comments } from '@/features/comments/components/comments';
import { useDiscussion } from '@/features/discussions/api/get-discussion';
import { DiscussionView } from '@/features/discussions/components/discussion-view';

export const Discussion = ({ discussionId }: { discussionId: string }) => {
const discussion = useDiscussion({ discussionId });

return (
<ContentLayout title={discussion?.data?.data?.title}>
<DiscussionView discussionId={discussionId} />
<div className="mt-8">
<ErrorBoundary
fallback={
<div>Failed to load comments. Try to refresh the page.</div>
}
>
<Comments discussionId={discussionId} />
</ErrorBoundary>
</div>
</ContentLayout>
);
};
94 changes: 59 additions & 35 deletions apps/nextjs-app/src/app/app/discussions/[discussionId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,47 +1,71 @@
'use client';
import {
dehydrate,
HydrationBoundary,
QueryClient,
} from '@tanstack/react-query';

import { useParams } from 'next/navigation';
import { ErrorBoundary } from 'react-error-boundary';
import { getInfiniteCommentsQueryOptions } from '@/features/comments/api/get-comments';
import {
getDiscussion,
getDiscussionQueryOptions,
} from '@/features/discussions/api/get-discussion';

import { ContentLayout } from '@/components/layouts/content-layout';
import { Spinner } from '@/components/ui/spinner';
import { Comments } from '@/features/comments/components/comments';
import { useDiscussion } from '@/features/discussions/api/get-discussion';
import { DiscussionView } from '@/features/discussions/components/discussion-view';
import { Discussion } from './_components/discussion';

const DiscussionPage = () => {
const params = useParams();
const discussionId = params?.discussionId as string;
export const generateMetadata = async ({
params,
}: {
params: Promise<{ discussionId: string }>;
}) => {
const discussionId = (await params).discussionId;

const discussionQuery = useDiscussion({
discussionId,
});
const discussion = await getDiscussion({ discussionId });

if (discussionQuery.isLoading) {
return (
<div className="flex h-48 w-full items-center justify-center">
<Spinner size="lg" />
</div>
);
}
return {
title: discussion.data?.title,
description: discussion.data?.title,
};
};

const preloadData = async (discussionId: string) => {
const queryClient = new QueryClient();

await Promise.all([
queryClient.prefetchQuery(getDiscussionQueryOptions(discussionId)),
queryClient.prefetchInfiniteQuery(
getInfiniteCommentsQueryOptions(discussionId),
),
]);

const discussion = discussionQuery.data?.data;
const dehydratedState = dehydrate(queryClient);

return {
dehydratedState,
queryClient,
};
};

const DiscussionPage = async ({
params,
}: {
params: Promise<{
discussionId: string;
}>;
}) => {
const discussionId = (await params).discussionId;

const { dehydratedState, queryClient } = await preloadData(discussionId);

const discussion = queryClient.getQueryData(
getDiscussionQueryOptions(discussionId).queryKey,
);

if (!discussion) return null;
if (!discussion?.data) return <div>Discussion not found</div>;

return (
<ContentLayout title={discussion.title}>
<DiscussionView discussionId={discussionId} />
<div className="mt-8">
<ErrorBoundary
fallback={
<div>Failed to load comments. Try to refresh the page.</div>
}
>
<Comments discussionId={discussionId} />
</ErrorBoundary>
</div>
</ContentLayout>
<HydrationBoundary state={dehydratedState}>
<Discussion discussionId={discussionId} />
</HydrationBoundary>
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from '@/testing/test-utils';
import { formatDate } from '@/utils/format';

import DiscussionsPage from '../page';
import { Discussions } from '../_components/discussions';

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

await waitForLoadingToFinish();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use client';

import { useQueryClient } from '@tanstack/react-query';

import { ContentLayout } from '@/components/layouts/content-layout';
import { getInfiniteCommentsQueryOptions } from '@/features/comments/api/get-comments';
import { CreateDiscussion } from '@/features/discussions/components/create-discussion';
import { DiscussionsList } from '@/features/discussions/components/discussions-list';

export const Discussions = () => {
const queryClient = useQueryClient();

return (
<ContentLayout title="Discussions">
<div className="flex justify-end">
<CreateDiscussion />
</div>
<div className="mt-4">
<DiscussionsList
onDiscussionPrefetch={(id) => {
// Prefetch the comments data when the user hovers over the link in the list
queryClient.prefetchInfiniteQuery(
getInfiniteCommentsQueryOptions(id),
);
}}
/>
</div>
</ContentLayout>
);
};
Loading

0 comments on commit 74ff903

Please sign in to comment.