diff --git a/apps/nextjs-app/e2e/tests/auth.setup.ts b/apps/nextjs-app/e2e/tests/auth.setup.ts index 187d10c9..1117cc38 100644 --- a/apps/nextjs-app/e2e/tests/auth.setup.ts +++ b/apps/nextjs-app/e2e/tests/auth.setup.ts @@ -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(); diff --git a/apps/nextjs-app/mock-server.ts b/apps/nextjs-app/mock-server.ts index 8e217267..d2eae318 100644 --- a/apps/nextjs-app/mock-server.ts +++ b/apps/nextjs-app/mock-server.ts @@ -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(() => { diff --git a/apps/nextjs-app/package.json b/apps/nextjs-app/package.json index ea64edf2..d5dcce00 100644 --- a/apps/nextjs-app/package.json +++ b/apps/nextjs-app/package.json @@ -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", @@ -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" diff --git a/apps/nextjs-app/src/app/app/_components/dashboard-info.tsx b/apps/nextjs-app/src/app/app/_components/dashboard-info.tsx new file mode 100644 index 00000000..92b6be5c --- /dev/null +++ b/apps/nextjs-app/src/app/app/_components/dashboard-info.tsx @@ -0,0 +1,34 @@ +'use client'; + +import { useUser } from '@/lib/auth'; + +export const DashboardInfo = () => { + const user = useUser(); + + return ( + <> +

+ Welcome {`${user.data?.firstName} ${user.data?.lastName}`} +

+

+ Your role is : {user.data?.role} +

+

In this application you can:

+ {user.data?.role === 'USER' && ( + + )} + {user.data?.role === 'ADMIN' && ( + + )} + + ); +}; diff --git a/apps/nextjs-app/src/components/layouts/dashboard-layout.tsx b/apps/nextjs-app/src/app/app/_components/dashboard-layout.tsx similarity index 84% rename from apps/nextjs-app/src/components/layouts/dashboard-layout.tsx rename to apps/nextjs-app/src/app/app/_components/dashboard-layout.tsx index 1c27b906..a84797b6 100644 --- a/apps/nextjs-app/src/components/layouts/dashboard-layout.tsx +++ b/apps/nextjs-app/src/app/app/_components/dashboard-layout.tsx @@ -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; @@ -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, @@ -152,7 +150,7 @@ const Layout = ({ children }: { children: React.ReactNode }) => { logout.mutate({})} + onClick={() => logout.mutate()} > Sign Out @@ -167,6 +165,10 @@ const Layout = ({ children }: { children: React.ReactNode }) => { ); }; +function Fallback({ error }: { error: Error }) { + return

Error: {error.message ?? 'Something went wrong!'}

; +} + export const DashboardLayout = ({ children, }: { @@ -175,28 +177,9 @@ export const DashboardLayout = ({ const pathname = usePathname(); return ( - - - - } - > - Something went wrong!} - > - ( -
- -
- )} - > - {children} -
-
-
+ + {children} +
); }; diff --git a/apps/nextjs-app/src/app/app/discussions/[discussionId]/__tests__/discussion.test.tsx b/apps/nextjs-app/src/app/app/discussions/[discussionId]/__tests__/discussion.test.tsx index c0e0e15e..30332bb3 100644 --- a/apps/nextjs-app/src/app/app/discussions/[discussionId]/__tests__/discussion.test.tsx +++ b/apps/nextjs-app/src/app/app/discussions/[discussionId]/__tests__/discussion.test.tsx @@ -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'); @@ -33,11 +33,14 @@ const renderDiscussion = async () => { vi.mocked(useParams).mockReturnValue({ discussionId: fakeDiscussion.id }); - const utils = await renderApp(, { - user: fakeUser, - path: `/app/discussions/:discussionId`, - url: `/app/discussions/${fakeDiscussion.id}`, - }); + const utils = await renderApp( + , + { + user: fakeUser, + path: `/app/discussions/:discussionId`, + url: `/app/discussions/${fakeDiscussion.id}`, + }, + ); await waitForLoadingToFinish(); diff --git a/apps/nextjs-app/src/app/app/discussions/[discussionId]/_components/discussion.tsx b/apps/nextjs-app/src/app/app/discussions/[discussionId]/_components/discussion.tsx new file mode 100644 index 00000000..e64d267d --- /dev/null +++ b/apps/nextjs-app/src/app/app/discussions/[discussionId]/_components/discussion.tsx @@ -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 ( + + +
+ Failed to load comments. Try to refresh the page.
+ } + > + + + +
+ ); +}; diff --git a/apps/nextjs-app/src/app/app/discussions/[discussionId]/page.tsx b/apps/nextjs-app/src/app/app/discussions/[discussionId]/page.tsx index d843e967..e92ce448 100644 --- a/apps/nextjs-app/src/app/app/discussions/[discussionId]/page.tsx +++ b/apps/nextjs-app/src/app/app/discussions/[discussionId]/page.tsx @@ -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 ( -
- -
- ); - } + 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
Discussion not found
; return ( - - -
- Failed to load comments. Try to refresh the page.
- } - > - - - -
+ + + ); }; diff --git a/apps/nextjs-app/src/app/app/discussions/__tests__/discussions.test.tsx b/apps/nextjs-app/src/app/app/discussions/__tests__/discussions.test.tsx index b94e3219..880ffdfb 100644 --- a/apps/nextjs-app/src/app/app/discussions/__tests__/discussions.test.tsx +++ b/apps/nextjs-app/src/app/app/discussions/__tests__/discussions.test.tsx @@ -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(() => {}); @@ -25,7 +25,7 @@ test( 'should create, render and delete discussions', { timeout: 10000 }, async () => { - await renderApp(); + await renderApp(); await waitForLoadingToFinish(); diff --git a/apps/nextjs-app/src/app/app/discussions/_components/discussions.tsx b/apps/nextjs-app/src/app/app/discussions/_components/discussions.tsx new file mode 100644 index 00000000..82adeba6 --- /dev/null +++ b/apps/nextjs-app/src/app/app/discussions/_components/discussions.tsx @@ -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 ( + +
+ +
+
+ { + // Prefetch the comments data when the user hovers over the link in the list + queryClient.prefetchInfiniteQuery( + getInfiniteCommentsQueryOptions(id), + ); + }} + /> +
+
+ ); +}; diff --git a/apps/nextjs-app/src/app/app/discussions/page.tsx b/apps/nextjs-app/src/app/app/discussions/page.tsx index bc002009..c1391196 100644 --- a/apps/nextjs-app/src/app/app/discussions/page.tsx +++ b/apps/nextjs-app/src/app/app/discussions/page.tsx @@ -1,31 +1,37 @@ -'use client'; +import { + dehydrate, + HydrationBoundary, + QueryClient, +} from '@tanstack/react-query'; -import { useQueryClient } from '@tanstack/react-query'; +import { getDiscussionsQueryOptions } from '@/features/discussions/api/get-discussions'; -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'; +import { Discussions } from './_components/discussions'; -const DiscussionsPage = () => { - const queryClient = useQueryClient(); +export const metadata = { + title: 'Discussions', + description: 'Discussions', +}; + +const DiscussionsPage = async ({ + searchParams, +}: { + searchParams: { page: string | null }; +}) => { + const queryClient = new QueryClient(); + + await queryClient.prefetchQuery( + getDiscussionsQueryOptions({ + page: searchParams.page ? Number(searchParams.page) : 1, + }), + ); + + const dehydratedState = dehydrate(queryClient); return ( - -
- -
-
- { - // Prefetch the comments data when the user hovers over the link in the list - queryClient.prefetchInfiniteQuery( - getInfiniteCommentsQueryOptions(id), - ); - }} - /> -
-
+ + + ); }; diff --git a/apps/nextjs-app/src/app/app/layout.tsx b/apps/nextjs-app/src/app/app/layout.tsx index 4c96e848..2089a6ef 100644 --- a/apps/nextjs-app/src/app/app/layout.tsx +++ b/apps/nextjs-app/src/app/app/layout.tsx @@ -1,6 +1,6 @@ import { ReactNode } from 'react'; -import { DashboardLayout } from '@/components/layouts/dashboard-layout'; +import { DashboardLayout } from './_components/dashboard-layout'; export const metadata = { title: 'Dashboard', diff --git a/apps/nextjs-app/src/app/app/page.tsx b/apps/nextjs-app/src/app/app/page.tsx index 6b68bea3..ae67926f 100644 --- a/apps/nextjs-app/src/app/app/page.tsx +++ b/apps/nextjs-app/src/app/app/page.tsx @@ -1,37 +1,12 @@ -'use client'; +import { DashboardInfo } from './_components/dashboard-info'; -import { useUser } from '@/lib/auth'; -import { ROLES } from '@/lib/authorization'; - -const DashboardPage = () => { - const user = useUser(); +export const metadata = { + title: 'Dashboard', + description: 'Dashboard', +}; - return ( - <> -

- Welcome {`${user.data?.firstName} ${user.data?.lastName}`} -

-

- Your role is : {user.data?.role} -

-

In this application you can:

- {user.data?.role === ROLES.USER && ( -
    -
  • Create comments in discussions
  • -
  • Delete own comments
  • -
- )} - {user.data?.role === ROLES.ADMIN && ( -
    -
  • Create discussions
  • -
  • Edit discussions
  • -
  • Delete discussions
  • -
  • Comment on discussions
  • -
  • Delete all comments
  • -
- )} - - ); +const DashboardPage = async () => { + return ; }; export default DashboardPage; diff --git a/apps/nextjs-app/src/app/app/profile/_components/profile.tsx b/apps/nextjs-app/src/app/app/profile/_components/profile.tsx new file mode 100644 index 00000000..e9b25ecc --- /dev/null +++ b/apps/nextjs-app/src/app/app/profile/_components/profile.tsx @@ -0,0 +1,48 @@ +'use client'; + +import { UpdateProfile } from '@/features/users/components/update-profile'; +import { useUser } from '@/lib/auth'; + +type EntryProps = { + label: string; + value: string; +}; +const Entry = ({ label, value }: EntryProps) => ( +
+
{label}
+
+ {value} +
+
+); + +export const Profile = () => { + const user = useUser(); + + if (!user) return null; + + return ( +
+
+
+

+ User Information +

+ +
+

+ Personal details of the user. +

+
+
+
+ + + + + +
+
+
+ ); +}; diff --git a/apps/nextjs-app/src/app/app/profile/page.tsx b/apps/nextjs-app/src/app/app/profile/page.tsx index 198064b6..ed893ca3 100644 --- a/apps/nextjs-app/src/app/app/profile/page.tsx +++ b/apps/nextjs-app/src/app/app/profile/page.tsx @@ -1,50 +1,12 @@ -'use client'; +import { Profile } from './_components/profile'; -import { UpdateProfile } from '@/features/users/components/update-profile'; -import { useUser } from '@/lib/auth'; - -type EntryProps = { - label: string; - value: string; +export const metadata = { + title: 'Profile', + description: 'Profile', }; -const Entry = ({ label, value }: EntryProps) => ( -
-
{label}
-
- {value} -
-
-); const ProfilePage = () => { - const user = useUser(); - - if (!user.data) return null; - - return ( -
-
-
-

- User Information -

- -
-

- Personal details of the user. -

-
-
-
- - - - - -
-
-
- ); + return ; }; export default ProfilePage; diff --git a/apps/nextjs-app/src/app/app/users/_components/admin-guard.tsx b/apps/nextjs-app/src/app/app/users/_components/admin-guard.tsx new file mode 100644 index 00000000..690157e5 --- /dev/null +++ b/apps/nextjs-app/src/app/app/users/_components/admin-guard.tsx @@ -0,0 +1,19 @@ +'use client'; + +import { Spinner } from '@/components/ui/spinner'; +import { useUser } from '@/lib/auth'; +import { canViewUsers } from '@/lib/authorization'; + +export const AdminGuard = ({ children }: { children: React.ReactNode }) => { + const user = useUser(); + + if (!user?.data) { + return ; + } + + if (!canViewUsers(user?.data)) { + return
Only admin can view this.
; + } + + return children; +}; diff --git a/apps/nextjs-app/src/app/app/users/_components/users.tsx b/apps/nextjs-app/src/app/app/users/_components/users.tsx new file mode 100644 index 00000000..669b40df --- /dev/null +++ b/apps/nextjs-app/src/app/app/users/_components/users.tsx @@ -0,0 +1,22 @@ +import { + dehydrate, + HydrationBoundary, + QueryClient, +} from '@tanstack/react-query'; + +import { getUsersQueryOptions } from '@/features/users/api/get-users'; +import { UsersList } from '@/features/users/components/users-list'; + +export const Users = async () => { + const queryClient = new QueryClient(); + + await queryClient.prefetchQuery(getUsersQueryOptions()); + + const dehydratedState = dehydrate(queryClient); + + return ( + + + + ); +}; diff --git a/apps/nextjs-app/src/app/app/users/page.tsx b/apps/nextjs-app/src/app/app/users/page.tsx index c6693e06..db32a2fb 100644 --- a/apps/nextjs-app/src/app/app/users/page.tsx +++ b/apps/nextjs-app/src/app/app/users/page.tsx @@ -1,18 +1,19 @@ -'use client'; - import { ContentLayout } from '@/components/layouts/content-layout'; -import { UsersList } from '@/features/users/components/users-list'; -import { Authorization, ROLES } from '@/lib/authorization'; + +import { AdminGuard } from './_components/admin-guard'; +import { Users } from './_components/users'; + +export const metadata = { + title: 'Users', + description: 'Users', +}; const UsersPage = () => { return ( - Only admin can view this.} - allowedRoles={[ROLES.ADMIN]} - > - - + + + ); }; diff --git a/apps/nextjs-app/src/components/layouts/auth-layout.tsx b/apps/nextjs-app/src/app/auth/_components/auth-layout.tsx similarity index 100% rename from apps/nextjs-app/src/components/layouts/auth-layout.tsx rename to apps/nextjs-app/src/app/auth/_components/auth-layout.tsx diff --git a/apps/nextjs-app/src/app/auth/layout.tsx b/apps/nextjs-app/src/app/auth/layout.tsx index 6ee7609b..d0afb440 100644 --- a/apps/nextjs-app/src/app/auth/layout.tsx +++ b/apps/nextjs-app/src/app/auth/layout.tsx @@ -1,9 +1,10 @@ import { ReactNode, Suspense } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; -import { AuthLayout as AuthLayoutComponent } from '@/components/layouts/auth-layout'; import { Spinner } from '@/components/ui/spinner'; +import { AuthLayout as AuthLayoutComponent } from './_components/auth-layout'; + export const metadata = { title: 'Bulletproof React', description: 'Welcome to Bulletproof React', diff --git a/apps/nextjs-app/src/app/auth/login/page.tsx b/apps/nextjs-app/src/app/auth/login/page.tsx index fcdfff01..0ffd05da 100644 --- a/apps/nextjs-app/src/app/auth/login/page.tsx +++ b/apps/nextjs-app/src/app/auth/login/page.tsx @@ -5,11 +5,6 @@ import { useRouter, useSearchParams } from 'next/navigation'; import { paths } from '@/config/paths'; import { LoginForm } from '@/features/auth/components/login-form'; -// export const metadata = { -// title: 'Log in to your account', -// description: 'Log in to your account', -// }; - const LoginPage = () => { const router = useRouter(); const searchParams = useSearchParams(); diff --git a/apps/nextjs-app/src/app/auth/register/page.tsx b/apps/nextjs-app/src/app/auth/register/page.tsx index aed4c1cb..99761c3b 100644 --- a/apps/nextjs-app/src/app/auth/register/page.tsx +++ b/apps/nextjs-app/src/app/auth/register/page.tsx @@ -7,11 +7,6 @@ import { paths } from '@/config/paths'; import { RegisterForm } from '@/features/auth/components/register-form'; import { useTeams } from '@/features/teams/api/get-teams'; -// export const metadata = { -// title: 'Register your account', -// description: 'Register your account', -// }; - const RegisterPage = () => { const router = useRouter(); diff --git a/apps/nextjs-app/src/app/layout.tsx b/apps/nextjs-app/src/app/layout.tsx index 4b9bc9eb..6bafa239 100644 --- a/apps/nextjs-app/src/app/layout.tsx +++ b/apps/nextjs-app/src/app/layout.tsx @@ -1,6 +1,12 @@ +import { + dehydrate, + HydrationBoundary, + QueryClient, +} from '@tanstack/react-query'; import { ReactNode } from 'react'; import { AppProvider } from '@/app/provider'; +import { getUserQueryOptions } from '@/lib/auth'; import '@/styles/globals.css'; @@ -9,14 +15,28 @@ export const metadata = { description: 'Showcasing Best Practices For Building React Applications', }; -const RootLayout = ({ children }: { children: ReactNode }) => { +const RootLayout = async ({ children }: { children: ReactNode }) => { + const queryClient = new QueryClient(); + + await queryClient.prefetchQuery(getUserQueryOptions()); + + const dehydratedState = dehydrate(queryClient); + return ( - {children} + + + {children} + + ); }; export default RootLayout; + +// We are not prerendering anything because the app is highly dynamic +// and the data depends on the user so we need to send cookies with each request +export const dynamic = 'force-dynamic'; diff --git a/apps/nextjs-app/src/app/public/discussions/[discussionId]/page.tsx b/apps/nextjs-app/src/app/public/discussions/[discussionId]/page.tsx index 532d05b5..92ffcb34 100644 --- a/apps/nextjs-app/src/app/public/discussions/[discussionId]/page.tsx +++ b/apps/nextjs-app/src/app/public/discussions/[discussionId]/page.tsx @@ -4,7 +4,7 @@ import { QueryClient, } from '@tanstack/react-query'; -import DiscussionPage from '@/app/app/discussions/[discussionId]/page'; +import { Discussion } from '@/app/app/discussions/[discussionId]/_components/discussion'; import { getInfiniteCommentsQueryOptions } from '@/features/comments/api/get-comments'; import { getDiscussion, @@ -14,26 +14,27 @@ import { export const generateMetadata = async ({ params, }: { - params: { discussionId: string }; + params: Promise<{ discussionId: string }>; }) => { - const discussion = await getDiscussion({ - discussionId: params.discussionId, - }); + const discussionId = (await params).discussionId; - const name = discussion.data.title; + const discussion = await getDiscussion({ discussionId }); return { - title: name, + title: discussion.data?.title, + description: discussion.data?.title, }; }; const preloadData = async (discussionId: string) => { const queryClient = new QueryClient(); - await queryClient.prefetchQuery(getDiscussionQueryOptions(discussionId)); - await queryClient.prefetchInfiniteQuery( - getInfiniteCommentsQueryOptions(discussionId), - ); + await Promise.all([ + queryClient.prefetchQuery(getDiscussionQueryOptions(discussionId)), + queryClient.prefetchInfiniteQuery( + getInfiniteCommentsQueryOptions(discussionId), + ), + ]); return { dehydratedState: dehydrate(queryClient), @@ -50,7 +51,7 @@ const PublicDiscussionPage = async ({ const { dehydratedState } = await preloadData(discussionId); return ( - + ); }; diff --git a/apps/nextjs-app/src/components/layouts/content-layout.tsx b/apps/nextjs-app/src/components/layouts/content-layout.tsx index bd5a193b..78499a1c 100644 --- a/apps/nextjs-app/src/components/layouts/content-layout.tsx +++ b/apps/nextjs-app/src/components/layouts/content-layout.tsx @@ -2,10 +2,10 @@ import { ReactNode } from 'react'; type ContentLayoutProps = { children: ReactNode; - title: string; + title?: string; }; -export const ContentLayout = ({ children, title }: ContentLayoutProps) => { +export const ContentLayout = ({ children, title = '' }: ContentLayoutProps) => { return (
diff --git a/apps/nextjs-app/src/features/comments/api/get-comments.ts b/apps/nextjs-app/src/features/comments/api/get-comments.ts index c53c00fd..84c07ee5 100644 --- a/apps/nextjs-app/src/features/comments/api/get-comments.ts +++ b/apps/nextjs-app/src/features/comments/api/get-comments.ts @@ -7,29 +7,23 @@ import { Comment, Meta } from '@/types/api'; export const getComments = ({ discussionId, page = 1, - cookie, }: { discussionId: string; page?: number; - cookie?: string; }): Promise<{ data: Comment[]; meta: Meta }> => { return api.get(`/comments`, { params: { discussionId, page, }, - cookie, }); }; -export const getInfiniteCommentsQueryOptions = ( - discussionId: string, - cookie?: string, -) => { +export const getInfiniteCommentsQueryOptions = (discussionId: string) => { return infiniteQueryOptions({ queryKey: ['comments', discussionId], queryFn: ({ pageParam = 1 }) => { - return getComments({ discussionId, page: pageParam as number, cookie }); + return getComments({ discussionId, page: pageParam as number }); }, getNextPageParam: (lastPage) => { if (lastPage?.meta?.page === lastPage?.meta?.totalPages) return undefined; diff --git a/apps/nextjs-app/src/features/comments/components/comments-list.tsx b/apps/nextjs-app/src/features/comments/components/comments-list.tsx index 0cde44d3..4b149662 100644 --- a/apps/nextjs-app/src/features/comments/components/comments-list.tsx +++ b/apps/nextjs-app/src/features/comments/components/comments-list.tsx @@ -7,8 +7,7 @@ import { Button } from '@/components/ui/button'; import { MDPreview } from '@/components/ui/md-preview'; import { Spinner } from '@/components/ui/spinner'; import { useUser } from '@/lib/auth'; -import { POLICIES, Authorization } from '@/lib/authorization'; -import { User } from '@/types/api'; +import { canDeleteComment } from '@/lib/authorization'; import { formatDate } from '@/utils/format'; import { useInfiniteComments } from '../api/get-comments'; @@ -68,15 +67,8 @@ export const CommentsList = ({ discussionId }: CommentsListProps) => { )}
- {!isPublicView && ( - - - + {!isPublicView && canDeleteComment(user.data, comment) && ( + )}
diff --git a/apps/nextjs-app/src/features/discussions/api/get-discussion.ts b/apps/nextjs-app/src/features/discussions/api/get-discussion.ts index 63fb9195..592ef623 100644 --- a/apps/nextjs-app/src/features/discussions/api/get-discussion.ts +++ b/apps/nextjs-app/src/features/discussions/api/get-discussion.ts @@ -6,23 +6,16 @@ import { Discussion } from '@/types/api'; export const getDiscussion = ({ discussionId, - cookie, }: { discussionId: string; - cookie?: string; }): Promise<{ data: Discussion }> => { - return api.get(`/discussions/${discussionId}`, { - cookie, - }); + return api.get(`/discussions/${discussionId}`); }; -export const getDiscussionQueryOptions = ( - discussionId: string, - cookie?: string, -) => { +export const getDiscussionQueryOptions = (discussionId: string) => { return queryOptions({ queryKey: ['discussions', discussionId], - queryFn: () => getDiscussion({ discussionId, cookie }), + queryFn: () => getDiscussion({ discussionId }), }); }; diff --git a/apps/nextjs-app/src/features/discussions/api/get-discussions.ts b/apps/nextjs-app/src/features/discussions/api/get-discussions.ts index f37dd4e3..10d75236 100644 --- a/apps/nextjs-app/src/features/discussions/api/get-discussions.ts +++ b/apps/nextjs-app/src/features/discussions/api/get-discussions.ts @@ -5,7 +5,7 @@ import { QueryConfig } from '@/lib/react-query'; import { Discussion, Meta } from '@/types/api'; export const getDiscussions = ( - { page, cookie }: { page?: number; cookie?: string } = { page: 1 }, + { page }: { page?: number } = { page: 1 }, ): Promise<{ data: Discussion[]; meta: Meta; @@ -14,17 +14,15 @@ export const getDiscussions = ( params: { page, }, - cookie, }); }; export const getDiscussionsQueryOptions = ({ - page, - cookie, -}: { page?: number; cookie?: string } = {}) => { + page = 1, +}: { page?: number } = {}) => { return queryOptions({ - queryKey: page ? ['discussions', { page }] : ['discussions'], - queryFn: () => getDiscussions({ page, cookie }), + queryKey: ['discussions', { page }], + queryFn: () => getDiscussions({ page }), }); }; diff --git a/apps/nextjs-app/src/features/discussions/components/create-discussion.tsx b/apps/nextjs-app/src/features/discussions/components/create-discussion.tsx index cef8e56f..65fd245f 100644 --- a/apps/nextjs-app/src/features/discussions/components/create-discussion.tsx +++ b/apps/nextjs-app/src/features/discussions/components/create-discussion.tsx @@ -12,7 +12,8 @@ import { Textarea, } from '@/components/ui/form'; import { useNotifications } from '@/components/ui/notifications'; -import { Authorization, ROLES } from '@/lib/authorization'; +import { useUser } from '@/lib/auth'; +import { canCreateDiscussion } from '@/lib/authorization'; import { createDiscussionInputSchema, @@ -32,69 +33,73 @@ export const CreateDiscussion = () => { }, }); + const user = useUser(); + + if (!canCreateDiscussion(user?.data)) { + return null; + } + return ( - - }> - Create Discussion - - } - title="Create Discussion" - submitButton={ - - } - > -
{ - createDiscussionMutation.mutate({ data: values }); - }} - schema={createDiscussionInputSchema} - options={{ - defaultValues: { - title: '', - body: '', - public: false, - }, - }} + }> + Create Discussion + + } + title="Create Discussion" + submitButton={ + + } + > + { + createDiscussionMutation.mutate({ data: values }); + }} + schema={createDiscussionInputSchema} + options={{ + defaultValues: { + title: '', + body: '', + public: false, + }, + }} + > + {({ register, formState, setValue, watch }) => ( + <> + -