diff --git a/apps/nextjs-app/.eslintrc.cjs b/apps/nextjs-app/.eslintrc.cjs index 410fbe9f..d8f31a04 100644 --- a/apps/nextjs-app/.eslintrc.cjs +++ b/apps/nextjs-app/.eslintrc.cjs @@ -140,7 +140,8 @@ module.exports = { }, { plugins: ['check-file'], - files: ['src/**/!(__tests__)/*'], + files: ['src/**/*'], + ignorePatterns: ['**/__tests__/**/*', 'src/app/**/*'], rules: { 'check-file/folder-naming-convention': [ 'error', diff --git a/apps/nextjs-app/next-env.d.ts b/apps/nextjs-app/next-env.d.ts index fd36f949..4f11a03d 100644 --- a/apps/nextjs-app/next-env.d.ts +++ b/apps/nextjs-app/next-env.d.ts @@ -1,6 +1,5 @@ /// /// -/// // NOTE: This file should not be edited // see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/apps/nextjs-app/package.json b/apps/nextjs-app/package.json index f0e7f8c7..06b5b876 100644 --- a/apps/nextjs-app/package.json +++ b/apps/nextjs-app/package.json @@ -99,7 +99,6 @@ "jsdom": "^24.0.0", "lint-staged": "^15.2.2", "msw": "^2.2.14", - "next-router-mock": "^0.9.13", "pino-http": "^10.1.0", "pino-pretty": "^11.1.0", "plop": "^4.0.1", diff --git a/apps/nextjs-app/src/app/pages/app/discussions/__tests__/discussion.test.tsx b/apps/nextjs-app/src/app/app/discussions/[discussionId]/__tests__/discussion.test.tsx similarity index 89% rename from apps/nextjs-app/src/app/pages/app/discussions/__tests__/discussion.test.tsx rename to apps/nextjs-app/src/app/app/discussions/[discussionId]/__tests__/discussion.test.tsx index 7afa3c00..c0e0e15e 100644 --- a/apps/nextjs-app/src/app/pages/app/discussions/__tests__/discussion.test.tsx +++ b/apps/nextjs-app/src/app/app/discussions/[discussionId]/__tests__/discussion.test.tsx @@ -1,4 +1,4 @@ -import mockRouter from 'next-router-mock'; +import { useParams } from 'next/navigation'; import { renderApp, @@ -11,13 +11,27 @@ import { waitForLoadingToFinish, } from '@/testing/test-utils'; -import { DiscussionPage } from '../discussion'; +import DiscussionPage from '../page'; + +vi.mock('next/navigation', async () => { + const actual = await vi.importActual('next/navigation'); + return { + ...actual, + useRouter: () => { + return { + push: vi.fn(), + replace: vi.fn(), + }; + }, + useParams: vi.fn(), + }; +}); const renderDiscussion = async () => { const fakeUser = await createUser(); const fakeDiscussion = await createDiscussion({ teamId: fakeUser.teamId }); - mockRouter.query = { discussionId: fakeDiscussion.id }; + vi.mocked(useParams).mockReturnValue({ discussionId: fakeDiscussion.id }); const utils = await renderApp(, { user: fakeUser, diff --git a/apps/nextjs-app/src/app/app/discussions/[discussionId]/page.tsx b/apps/nextjs-app/src/app/app/discussions/[discussionId]/page.tsx new file mode 100644 index 00000000..b4c11212 --- /dev/null +++ b/apps/nextjs-app/src/app/app/discussions/[discussionId]/page.tsx @@ -0,0 +1,49 @@ +'use client'; + +import { useParams } from 'next/navigation'; +import { ErrorBoundary } from 'react-error-boundary'; + +import { ContentLayout } from '@/components/layouts'; +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'; + +const DiscussionPage = () => { + const params = useParams(); + const discussionId = params?.discussionId as string; + + const discussionQuery = useDiscussion({ + discussionId, + }); + + if (discussionQuery.isLoading) { + return ( +
+ +
+ ); + } + + const discussion = discussionQuery.data?.data; + + if (!discussion) return null; + + return ( + + +
+ Failed to load comments. Try to refresh the page.
+ } + > + + + +
+ ); +}; + +export default DiscussionPage; diff --git a/apps/nextjs-app/src/app/pages/app/discussions/__tests__/discussions.test.tsx b/apps/nextjs-app/src/app/app/discussions/__tests__/discussions.test.tsx similarity index 97% rename from apps/nextjs-app/src/app/pages/app/discussions/__tests__/discussions.test.tsx rename to apps/nextjs-app/src/app/app/discussions/__tests__/discussions.test.tsx index de253e36..b94e3219 100644 --- a/apps/nextjs-app/src/app/pages/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 '../discussions'; +import DiscussionsPage from '../page'; beforeAll(() => { vi.spyOn(console, 'error').mockImplementation(() => {}); diff --git a/apps/nextjs-app/src/app/pages/app/discussions/discussions.tsx b/apps/nextjs-app/src/app/app/discussions/page.tsx similarity index 69% rename from apps/nextjs-app/src/app/pages/app/discussions/discussions.tsx rename to apps/nextjs-app/src/app/app/discussions/page.tsx index afce34ae..a9831757 100644 --- a/apps/nextjs-app/src/app/pages/app/discussions/discussions.tsx +++ b/apps/nextjs-app/src/app/app/discussions/page.tsx @@ -1,16 +1,17 @@ +'use client'; + import { useQueryClient } from '@tanstack/react-query'; -import { ReactElement } from 'react'; -import { ContentLayout, DashboardLayout } from '@/components/layouts'; +import { ContentLayout } from '@/components/layouts'; 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 DiscussionsPage = () => { +const DiscussionsPage = () => { const queryClient = useQueryClient(); return ( - <> +
@@ -24,14 +25,8 @@ export const DiscussionsPage = () => { }} /> - +
); }; -DiscussionsPage.getLayout = (page: ReactElement) => { - return ( - - {page} - - ); -}; +export default DiscussionsPage; diff --git a/apps/nextjs-app/src/app/app/layout.tsx b/apps/nextjs-app/src/app/app/layout.tsx new file mode 100644 index 00000000..152c90da --- /dev/null +++ b/apps/nextjs-app/src/app/app/layout.tsx @@ -0,0 +1,7 @@ +import { ReactNode } from 'react'; + +import { DashboardLayout } from '@/components/layouts'; + +export default function AppLayout({ children }: { children: ReactNode }) { + return {children}; +} diff --git a/apps/nextjs-app/src/app/pages/app/dashboard.tsx b/apps/nextjs-app/src/app/app/page.tsx similarity index 72% rename from apps/nextjs-app/src/app/pages/app/dashboard.tsx rename to apps/nextjs-app/src/app/app/page.tsx index 9035fa5d..6b68bea3 100644 --- a/apps/nextjs-app/src/app/pages/app/dashboard.tsx +++ b/apps/nextjs-app/src/app/app/page.tsx @@ -1,12 +1,10 @@ -import { ReactElement } from 'react'; +'use client'; -import { ContentLayout, DashboardLayout } from '@/components/layouts'; import { useUser } from '@/lib/auth'; import { ROLES } from '@/lib/authorization'; -export const DashboardPage = () => { +const DashboardPage = () => { const user = useUser(); - if (!user.data) return null; return ( <> @@ -36,10 +34,4 @@ export const DashboardPage = () => { ); }; -DashboardPage.getLayout = (page: ReactElement) => { - return ( - - {page} - - ); -}; +export default DashboardPage; diff --git a/apps/nextjs-app/src/app/pages/app/profile.tsx b/apps/nextjs-app/src/app/app/profile/page.tsx similarity index 82% rename from apps/nextjs-app/src/app/pages/app/profile.tsx rename to apps/nextjs-app/src/app/app/profile/page.tsx index 744a6927..198064b6 100644 --- a/apps/nextjs-app/src/app/pages/app/profile.tsx +++ b/apps/nextjs-app/src/app/app/profile/page.tsx @@ -1,6 +1,5 @@ -import { ReactElement } from 'react'; +'use client'; -import { ContentLayout, DashboardLayout } from '@/components/layouts'; import { UpdateProfile } from '@/features/users/components/update-profile'; import { useUser } from '@/lib/auth'; @@ -17,7 +16,7 @@ const Entry = ({ label, value }: EntryProps) => ( ); -export const ProfilePage = () => { +const ProfilePage = () => { const user = useUser(); if (!user.data) return null; @@ -48,10 +47,4 @@ export const ProfilePage = () => { ); }; -ProfilePage.getLayout = (page: ReactElement) => { - return ( - - {page} - - ); -}; +export default ProfilePage; diff --git a/apps/nextjs-app/src/app/pages/app/users.tsx b/apps/nextjs-app/src/app/app/users/page.tsx similarity index 54% rename from apps/nextjs-app/src/app/pages/app/users.tsx rename to apps/nextjs-app/src/app/app/users/page.tsx index b0524e32..14bb2eba 100644 --- a/apps/nextjs-app/src/app/pages/app/users.tsx +++ b/apps/nextjs-app/src/app/app/users/page.tsx @@ -1,10 +1,10 @@ -import { ReactElement } from 'react'; +'use client'; -import { ContentLayout, DashboardLayout } from '@/components/layouts'; +import { ContentLayout } from '@/components/layouts'; import { UsersList } from '@/features/users/components/users-list'; import { Authorization, ROLES } from '@/lib/authorization'; -export const UsersPage = () => { +const UsersPage = () => { return ( { ); }; -UsersPage.getLayout = (page: ReactElement) => { - return ( - - {page} - - ); -}; +export default UsersPage; diff --git a/apps/nextjs-app/src/app/auth/layout.tsx b/apps/nextjs-app/src/app/auth/layout.tsx new file mode 100644 index 00000000..830e0fe8 --- /dev/null +++ b/apps/nextjs-app/src/app/auth/layout.tsx @@ -0,0 +1,16 @@ +'use client'; + +import { usePathname } from 'next/navigation'; +import { ReactNode } from 'react'; + +import { AuthLayout as AuthLayoutComponent } from '@/components/layouts/auth-layout'; + +export default function AuthLayout({ children }: { children: ReactNode }) { + const pathname = usePathname(); + const isLoginPage = pathname === '/auth/login'; + const title = isLoginPage + ? 'Log in to your account' + : 'Register your account'; + + return {children}; +} diff --git a/apps/nextjs-app/src/app/auth/login/page.tsx b/apps/nextjs-app/src/app/auth/login/page.tsx new file mode 100644 index 00000000..b779aef0 --- /dev/null +++ b/apps/nextjs-app/src/app/auth/login/page.tsx @@ -0,0 +1,26 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; + +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(); + const redirectTo = searchParams?.get('redirectTo'); + + return ( + + router.replace(`${redirectTo ? `${redirectTo}` : '/app'}`) + } + /> + ); +}; + +export default LoginPage; diff --git a/apps/nextjs-app/src/app/pages/auth/register.tsx b/apps/nextjs-app/src/app/auth/register/page.tsx similarity index 60% rename from apps/nextjs-app/src/app/pages/auth/register.tsx rename to apps/nextjs-app/src/app/auth/register/page.tsx index d3858967..87ca70fd 100644 --- a/apps/nextjs-app/src/app/pages/auth/register.tsx +++ b/apps/nextjs-app/src/app/auth/register/page.tsx @@ -1,14 +1,21 @@ -import { useRouter } from 'next/router'; -import { ReactElement, useState } from 'react'; +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import { useState } from 'react'; -import { AuthLayout } from '@/components/layouts/auth-layout'; import { RegisterForm } from '@/features/auth/components/register-form'; import { useTeams } from '@/features/teams/api/get-teams'; -export const RegisterPage = () => { +// export const metadata = { +// title: 'Register your account', +// description: 'Register your account', +// }; + +const RegisterPage = () => { const router = useRouter(); - const { redirectTo } = router.query; + const searchParams = useSearchParams(); + const redirectTo = searchParams?.get('redirectTo'); const [chooseTeam, setChooseTeam] = useState(false); @@ -30,6 +37,4 @@ export const RegisterPage = () => { ); }; -RegisterPage.getLayout = (page: ReactElement) => { - return {page}; -}; +export default RegisterPage; diff --git a/apps/nextjs-app/src/app/layout.tsx b/apps/nextjs-app/src/app/layout.tsx new file mode 100644 index 00000000..f55b4141 --- /dev/null +++ b/apps/nextjs-app/src/app/layout.tsx @@ -0,0 +1,15 @@ +import { ReactNode } from 'react'; + +import { AppProvider } from '@/app/provider'; + +import '@/styles/globals.css'; + +export default function RootLayout({ children }: { children: ReactNode }) { + return ( + + + {children} + + + ); +} diff --git a/apps/nextjs-app/src/pages/404.tsx b/apps/nextjs-app/src/app/not-found.tsx similarity index 100% rename from apps/nextjs-app/src/pages/404.tsx rename to apps/nextjs-app/src/app/not-found.tsx diff --git a/apps/nextjs-app/src/app/page.tsx b/apps/nextjs-app/src/app/page.tsx new file mode 100644 index 00000000..5cfc008a --- /dev/null +++ b/apps/nextjs-app/src/app/page.tsx @@ -0,0 +1,88 @@ +'use client'; +import { useRouter } from 'next/navigation'; + +import { Button } from '@/components/ui/button'; +import { useUser } from '@/lib/auth'; + +// export const metadata = { +// title: 'Bulletproof React', +// description: 'Welcome to bulletproof react', +// }; + +export const HomePage = () => { + const router = useRouter(); + const user = useUser(); + + const handleStart = () => { + if (user.data) { + router.push('/app'); + } else { + router.push('/auth/login'); + } + }; + + return ( +
+
+

+ Bulletproof React +

+ react +

Showcasing Best Practices For Building React Applications

+
+
+ +
+ +
+
+
+ ); +}; + +export default HomePage; diff --git a/apps/nextjs-app/src/app/pages/app/discussions/discussion.tsx b/apps/nextjs-app/src/app/pages/app/discussions/discussion.tsx deleted file mode 100644 index fcf8d683..00000000 --- a/apps/nextjs-app/src/app/pages/app/discussions/discussion.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { - dehydrate, - HydrationBoundary, - QueryClient, -} from '@tanstack/react-query'; -import { GetServerSideProps, InferGetServerSidePropsType } from 'next'; -import { useRouter } from 'next/router'; -import { ReactElement } from 'react'; -import { ErrorBoundary } from 'react-error-boundary'; - -import { ContentLayout, DashboardLayout } from '@/components/layouts'; -import { Spinner } from '@/components/ui/spinner'; -import { getInfiniteCommentsQueryOptions } from '@/features/comments/api/get-comments'; -import { Comments } from '@/features/comments/components/comments'; -import { - useDiscussion, - getDiscussionQueryOptions, -} from '@/features/discussions/api/get-discussion'; -import { DiscussionView } from '@/features/discussions/components/discussion-view'; - -type DiscussionPageProps = { - dehydratedState?: unknown; -}; - -export const getServerSideProps = (async ({ query, req }) => { - const queryClient = new QueryClient(); - const discussionId = query.discussionId as string; - const cookie = req.headers.cookie; - - await queryClient.prefetchQuery( - getDiscussionQueryOptions(discussionId, cookie), - ); - await queryClient.prefetchInfiniteQuery( - getInfiniteCommentsQueryOptions(discussionId, cookie), - ); - - return { - props: { - dehydratedState: dehydrate(queryClient), - }, - }; -}) satisfies GetServerSideProps; - -export const DiscussionPage = () => { - const router = useRouter(); - const discussionId = router.query.discussionId as string; - - const discussionQuery = useDiscussion({ - discussionId, - }); - - if (discussionQuery.isLoading) { - return ( -
- -
- ); - } - - const discussion = discussionQuery.data?.data; - - if (!discussion) return null; - - return ( - - -
- Failed to load comments. Try to refresh the page.
- } - > - - - -
- ); -}; - -DiscussionPage.getLayout = (page: ReactElement) => { - return {page}; -}; - -export const PublicDiscussionPage = ({ - dehydratedState, -}: InferGetServerSidePropsType) => { - return ( - - - - ); -}; diff --git a/apps/nextjs-app/src/app/pages/auth/login.tsx b/apps/nextjs-app/src/app/pages/auth/login.tsx deleted file mode 100644 index 69e9d238..00000000 --- a/apps/nextjs-app/src/app/pages/auth/login.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { useRouter } from 'next/router'; -import { ReactElement } from 'react'; - -import { AuthLayout } from '@/components/layouts/auth-layout'; -import { LoginForm } from '@/features/auth/components/login-form'; - -export const LoginPage = () => { - const router = useRouter(); - const { redirectTo } = router.query; - - return ( - - router.replace(`${redirectTo ? `${redirectTo}` : '/app'}`) - } - /> - ); -}; - -LoginPage.getLayout = (page: ReactElement) => { - return {page}; -}; diff --git a/apps/nextjs-app/src/app/provider.tsx b/apps/nextjs-app/src/app/provider.tsx index cb269351..11b87a8e 100644 --- a/apps/nextjs-app/src/app/provider.tsx +++ b/apps/nextjs-app/src/app/provider.tsx @@ -1,3 +1,5 @@ +'use client'; + import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import * as React from 'react'; @@ -5,7 +7,6 @@ import { ErrorBoundary } from 'react-error-boundary'; import { MainErrorFallback } from '@/components/errors/main'; import { Notifications } from '@/components/ui/notifications'; -import { Spinner } from '@/components/ui/spinner'; import { queryConfig } from '@/lib/react-query'; type AppProviderProps = { @@ -21,20 +22,12 @@ export const AppProvider = ({ children }: AppProviderProps) => { ); return ( - - - - } - > - - - {process.env.DEV && } - - {children} - - - + + + {process.env.DEV && } + + {children} + + ); }; diff --git a/apps/nextjs-app/src/app/public/discussions/[discussionId]/page.tsx b/apps/nextjs-app/src/app/public/discussions/[discussionId]/page.tsx new file mode 100644 index 00000000..97cfffdf --- /dev/null +++ b/apps/nextjs-app/src/app/public/discussions/[discussionId]/page.tsx @@ -0,0 +1,47 @@ +import { + dehydrate, + HydrationBoundary, + QueryClient, +} from '@tanstack/react-query'; + +import { getInfiniteCommentsQueryOptions } from '@/features/comments/api/get-comments'; +import { getDiscussionQueryOptions } from '@/features/discussions/api/get-discussion'; + +import DiscussionPage from '@/app/app/discussions/[discussionId]/page'; +import { cookies } from 'next/headers'; + +const preloadData = async (discussionId: string) => { + const queryClient = new QueryClient(); + + const cookieStore = cookies(); + + const cookie = cookieStore.get('bulletproof_react_app_token')?.value; + + await queryClient.prefetchQuery( + getDiscussionQueryOptions(discussionId, cookie), + ); + await queryClient.prefetchInfiniteQuery( + getInfiniteCommentsQueryOptions(discussionId, cookie), + ); + + return { + dehydratedState: dehydrate(queryClient), + }; +}; + +const PublicDiscussionPage = async ({ + params: { discussionId }, +}: { + params: { + discussionId: string; + }; +}) => { + const { dehydratedState } = await preloadData(discussionId); + return ( + + + + ); +}; + +export default PublicDiscussionPage; diff --git a/apps/nextjs-app/src/components/layouts/auth-layout.tsx b/apps/nextjs-app/src/components/layouts/auth-layout.tsx index 30369771..95997b38 100644 --- a/apps/nextjs-app/src/components/layouts/auth-layout.tsx +++ b/apps/nextjs-app/src/components/layouts/auth-layout.tsx @@ -1,8 +1,9 @@ -import { useRouter } from 'next/router'; +'use client'; + +import { useRouter } from 'next/navigation'; import * as React from 'react'; import { useEffect } from 'react'; -import { Head } from '@/components/seo'; import { Link } from '@/components/ui/link'; import { useUser } from '@/lib/auth'; @@ -11,6 +12,11 @@ type LayoutProps = { title: string; }; +// export const metadata = { +// title: 'Bulletproof React', +// description: 'Welcome to bulletproof react', +// }; + export const AuthLayout = ({ children, title }: LayoutProps) => { const user = useUser(); @@ -23,27 +29,24 @@ export const AuthLayout = ({ children, title }: LayoutProps) => { }, [user.data, router]); return ( - <> - -
-
-
- - Workflow - -
- -

- {title} -

+
+
+
+ + Workflow +
-
-
- {children} -
+

+ {title} +

+
+ +
+
+ {children}
- +
); }; diff --git a/apps/nextjs-app/src/components/layouts/content-layout.tsx b/apps/nextjs-app/src/components/layouts/content-layout.tsx index 8b09c411..a6e7edd6 100644 --- a/apps/nextjs-app/src/components/layouts/content-layout.tsx +++ b/apps/nextjs-app/src/components/layouts/content-layout.tsx @@ -1,7 +1,5 @@ import * as React from 'react'; -import { Head } from '../seo'; - type ContentLayoutProps = { children: React.ReactNode; title: string; @@ -9,16 +7,13 @@ type ContentLayoutProps = { export const ContentLayout = ({ children, title }: ContentLayoutProps) => { return ( - <> - -
-
-

{title}

-
-
- {children} -
+
+
+

{title}

+
+
+ {children}
- +
); }; diff --git a/apps/nextjs-app/src/components/layouts/dashboard-layout.tsx b/apps/nextjs-app/src/components/layouts/dashboard-layout.tsx index c4c07cb9..d5a52468 100644 --- a/apps/nextjs-app/src/components/layouts/dashboard-layout.tsx +++ b/apps/nextjs-app/src/components/layouts/dashboard-layout.tsx @@ -1,7 +1,9 @@ +'use client'; + import { Home, PanelLeft, Folder, Users, User2 } from 'lucide-react'; import NextLink from 'next/link'; -import { useRouter } from 'next/router'; -import { useEffect, useState, Suspense } from 'react'; +import { useRouter, usePathname } from 'next/navigation'; +import { Suspense } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; import { Button } from '@/components/ui/button'; @@ -37,62 +39,10 @@ const Logo = () => { ); }; -const Progress = () => { - const router = useRouter(); - const [progress, setProgress] = useState(0); - - useEffect(() => { - const handleRouteChangeStart = () => { - setProgress(0); - const timer = setInterval(() => { - setProgress((oldProgress) => { - if (oldProgress === 100) { - clearInterval(timer); - return 100; - } - const newProgress = oldProgress + 10; - return newProgress > 100 ? 100 : newProgress; - }); - }, 300); - - return () => { - clearInterval(timer); - }; - }; - - const handleRouteChangeComplete = () => { - setProgress(100); - setTimeout(() => { - setProgress(0); - }, 500); // Adjust the delay as needed - }; - - router.events.on('routeChangeStart', handleRouteChangeStart); - router.events.on('routeChangeComplete', handleRouteChangeComplete); - router.events.on('routeChangeError', handleRouteChangeComplete); - - return () => { - router.events.off('routeChangeStart', handleRouteChangeStart); - router.events.off('routeChangeComplete', handleRouteChangeComplete); - router.events.off('routeChangeError', handleRouteChangeComplete); - }; - }, [router.events]); - - if (progress === 0) { - return null; - } - - return ( -
- ); -}; - const Layout = ({ children }: { children: React.ReactNode }) => { const logout = useLogout(); const { checkAccess } = useAuthorization(); + const pathname = usePathname(); const router = useRouter(); const navigation = [ { name: 'Dashboard', to: '/app', icon: Home }, @@ -112,7 +62,7 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
{navigation.map((item) => { - const isActive = router.pathname === item.to; + const isActive = pathname === item.to; return ( {
- + {/* */}
{navigation.map((item) => { - const isActive = router.pathname === item.to; + const isActive = pathname === item.to; return ( { - const router = useRouter(); + const pathname = usePathname(); return ( Something went wrong!
} > { - return ( - - {title} - - - ); -}; diff --git a/apps/nextjs-app/src/components/seo/index.ts b/apps/nextjs-app/src/components/seo/index.ts deleted file mode 100644 index b6f0ab75..00000000 --- a/apps/nextjs-app/src/components/seo/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './head'; diff --git a/apps/nextjs-app/src/features/auth/components/login-form.tsx b/apps/nextjs-app/src/features/auth/components/login-form.tsx index a038647d..82dac23f 100644 --- a/apps/nextjs-app/src/features/auth/components/login-form.tsx +++ b/apps/nextjs-app/src/features/auth/components/login-form.tsx @@ -1,5 +1,7 @@ +'use client'; + import NextLink from 'next/link'; -import { useRouter } from 'next/router'; +import { useSearchParams } from 'next/navigation'; import { Button } from '@/components/ui/button'; import { Form, Input } from '@/components/ui/form'; @@ -13,9 +15,9 @@ export const LoginForm = ({ onSuccess }: LoginFormProps) => { const login = useLogin({ onSuccess, }); - const router = useRouter(); - const redirectTo = router.query.redirectTo as string | undefined; + const searchParams = useSearchParams(); + const redirectTo = searchParams?.get('redirectTo'); return (
{ const registering = useRegister({ onSuccess }); - const router = useRouter(); - const redirectTo = router.query.redirectTo as string | undefined; + const searchParams = useSearchParams(); + const redirectTo = searchParams?.get('redirectTo'); return (
diff --git a/apps/nextjs-app/src/features/discussions/components/discussions-list.tsx b/apps/nextjs-app/src/features/discussions/components/discussions-list.tsx index d532d5c7..acfa384a 100644 --- a/apps/nextjs-app/src/features/discussions/components/discussions-list.tsx +++ b/apps/nextjs-app/src/features/discussions/components/discussions-list.tsx @@ -1,5 +1,5 @@ import { useQueryClient } from '@tanstack/react-query'; -import { useRouter } from 'next/router'; +import { useSearchParams } from 'next/navigation'; import { Link } from '@/components/ui/link'; import { Spinner } from '@/components/ui/spinner'; @@ -18,8 +18,8 @@ export type DiscussionsListProps = { export const DiscussionsList = ({ onDiscussionPrefetch, }: DiscussionsListProps) => { - const router = useRouter(); - const page = router.query.page ? Number(router.query.page) : 1; + const searchParams = useSearchParams(); + const page = searchParams?.get('page') ? Number(searchParams.get('page')) : 1; const discussionsQuery = useDiscussions({ page: page, diff --git a/apps/nextjs-app/src/lib/api-client.ts b/apps/nextjs-app/src/lib/api-client.ts index 93131e6e..8455e77a 100644 --- a/apps/nextjs-app/src/lib/api-client.ts +++ b/apps/nextjs-app/src/lib/api-client.ts @@ -29,18 +29,6 @@ api.interceptors.response.use( message, }); - if (error.response?.status === 401) { - if (typeof window !== 'undefined') { - const searchParams = new URLSearchParams(); - const redirectTo = searchParams.get('redirectTo'); - if (redirectTo) { - window.location.href = `/auth/login?redirectTo=${redirectTo}`; - } else { - window.location.href = '/auth/login'; - } - } - } - return Promise.reject(error); }, ); diff --git a/apps/nextjs-app/src/lib/auth.tsx b/apps/nextjs-app/src/lib/auth.tsx index c3288274..aaadba46 100644 --- a/apps/nextjs-app/src/lib/auth.tsx +++ b/apps/nextjs-app/src/lib/auth.tsx @@ -1,4 +1,6 @@ -import { useRouter } from 'next/router'; +'use client'; + +import { usePathname, useRouter } from 'next/navigation'; import { useEffect } from 'react'; import { configureAuth } from 'react-query-auth'; import { z } from 'zod'; @@ -78,14 +80,19 @@ export const { useUser, useLogin, useLogout, useRegister, AuthLoader } = export const ProtectedRoute = ({ children }: { children: React.ReactNode }) => { const user = useUser(); const router = useRouter(); + const pathname = usePathname(); useEffect(() => { if (!user.data) { - router.replace( - `/auth/login?redirectTo=${encodeURIComponent(router.pathname)}`, - ); + if (pathname) { + router.replace( + `/auth/login?redirectTo=${encodeURIComponent(pathname)}`, + ); + } else { + router.replace('/auth/login'); + } } - }, [user.data, router]); + }, [user.data, router, pathname]); return children; }; diff --git a/apps/nextjs-app/src/lib/authorization.tsx b/apps/nextjs-app/src/lib/authorization.tsx index fbf094a2..72297b75 100644 --- a/apps/nextjs-app/src/lib/authorization.tsx +++ b/apps/nextjs-app/src/lib/authorization.tsx @@ -1,4 +1,6 @@ -import { useRouter } from 'next/router'; +'use client'; + +import { usePathname, useRouter } from 'next/navigation'; import * as React from 'react'; import { Comment, User } from '@/types/api'; @@ -29,10 +31,11 @@ export const POLICIES = { export const useAuthorization = () => { const user = useUser(); const router = useRouter(); + const pathname = usePathname(); if (!user.data && !user.isLoading) { - const redirectTo = encodeURIComponent(router.pathname); - window.location.href = `/auth/login?redirectTo=${redirectTo}`; + const redirectTo = encodeURIComponent(pathname); + router.push(`/auth/login?redirectTo=${redirectTo}`); } const checkAccess = React.useCallback( diff --git a/apps/nextjs-app/src/pages/_app.tsx b/apps/nextjs-app/src/pages/_app.tsx deleted file mode 100644 index 90852dcf..00000000 --- a/apps/nextjs-app/src/pages/_app.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { NextPage } from 'next'; -import type { AppProps } from 'next/app'; -import { ReactElement, ReactNode } from 'react'; - -import { AppProvider } from '@/app/provider'; - -import '@/styles/globals.css'; - -// eslint-disable-next-line @typescript-eslint/ban-types -export type NextPageWithLayout

= NextPage & { - getLayout?: (page: ReactElement) => ReactNode; -}; - -type AppPropsWithLayout = AppProps & { - Component: NextPageWithLayout; -}; - -export default function App({ Component, pageProps }: AppPropsWithLayout) { - const getLayout = Component.getLayout ?? ((page) => page); - return {getLayout()}; -} diff --git a/apps/nextjs-app/src/pages/app/discussions/[discussionId].tsx b/apps/nextjs-app/src/pages/app/discussions/[discussionId].tsx deleted file mode 100644 index 4dda5581..00000000 --- a/apps/nextjs-app/src/pages/app/discussions/[discussionId].tsx +++ /dev/null @@ -1 +0,0 @@ -export { DiscussionPage as default } from '@/app/pages/app/discussions/discussion'; diff --git a/apps/nextjs-app/src/pages/app/discussions/index.tsx b/apps/nextjs-app/src/pages/app/discussions/index.tsx deleted file mode 100644 index baad9a2a..00000000 --- a/apps/nextjs-app/src/pages/app/discussions/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { DiscussionsPage as default } from '@/app/pages/app/discussions/discussions'; diff --git a/apps/nextjs-app/src/pages/app/index.tsx b/apps/nextjs-app/src/pages/app/index.tsx deleted file mode 100644 index 56da8442..00000000 --- a/apps/nextjs-app/src/pages/app/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { DashboardPage as default } from '@/app/pages/app/dashboard'; diff --git a/apps/nextjs-app/src/pages/app/profile.tsx b/apps/nextjs-app/src/pages/app/profile.tsx deleted file mode 100644 index 64b53045..00000000 --- a/apps/nextjs-app/src/pages/app/profile.tsx +++ /dev/null @@ -1 +0,0 @@ -export { ProfilePage as default } from '@/app/pages/app/profile'; diff --git a/apps/nextjs-app/src/pages/app/users.tsx b/apps/nextjs-app/src/pages/app/users.tsx deleted file mode 100644 index f7bffe43..00000000 --- a/apps/nextjs-app/src/pages/app/users.tsx +++ /dev/null @@ -1 +0,0 @@ -export { UsersPage as default } from '@/app/pages/app/users'; diff --git a/apps/nextjs-app/src/pages/auth/login.tsx b/apps/nextjs-app/src/pages/auth/login.tsx deleted file mode 100644 index 21170557..00000000 --- a/apps/nextjs-app/src/pages/auth/login.tsx +++ /dev/null @@ -1 +0,0 @@ -export { LoginPage as default } from '@/app/pages/auth/login'; diff --git a/apps/nextjs-app/src/pages/auth/register.tsx b/apps/nextjs-app/src/pages/auth/register.tsx deleted file mode 100644 index 2fa77b12..00000000 --- a/apps/nextjs-app/src/pages/auth/register.tsx +++ /dev/null @@ -1 +0,0 @@ -export { RegisterPage as default } from '@/app/pages/auth/register'; diff --git a/apps/nextjs-app/src/pages/index.tsx b/apps/nextjs-app/src/pages/index.tsx deleted file mode 100644 index dbe1e60b..00000000 --- a/apps/nextjs-app/src/pages/index.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { useRouter } from 'next/router'; - -import { Head } from '@/components/seo'; -import { Button } from '@/components/ui/button'; -import { useUser } from '@/lib/auth'; - -export const HomePage = () => { - const router = useRouter(); - const user = useUser(); - - const handleStart = () => { - if (user.data) { - router.push('/app'); - } else { - router.push('/auth/login'); - } - }; - - return ( - <> - -

-
-

- Bulletproof React -

- react -

Showcasing Best Practices For Building React Applications

-
-
- -
- -
-
-
- - ); -}; - -export default HomePage; diff --git a/apps/nextjs-app/src/pages/public/discussions/[discussionId].tsx b/apps/nextjs-app/src/pages/public/discussions/[discussionId].tsx deleted file mode 100644 index 79b146bf..00000000 --- a/apps/nextjs-app/src/pages/public/discussions/[discussionId].tsx +++ /dev/null @@ -1,4 +0,0 @@ -export { - getServerSideProps, - PublicDiscussionPage as default, -} from '@/app/pages/app/discussions/discussion'; diff --git a/apps/nextjs-app/src/testing/setup-tests.ts b/apps/nextjs-app/src/testing/setup-tests.ts index 3a9c11aa..4bedb78c 100644 --- a/apps/nextjs-app/src/testing/setup-tests.ts +++ b/apps/nextjs-app/src/testing/setup-tests.ts @@ -7,7 +7,22 @@ vi.mock('zustand'); beforeAll(() => { server.listen({ onUnhandledRequest: 'error' }); - vi.mock('next/router', () => require('next-router-mock')); + vi.mock('next/navigation', async () => { + const actual = await vi.importActual('next/navigation'); + return { + ...actual, + useRouter: () => { + return { + push: vi.fn(), + replace: vi.fn(), + }; + }, + usePathname: () => '/app', + useSearchParams: () => ({ + get: vi.fn(), + }), + }; + }); }); afterAll(() => server.close()); beforeEach(() => { diff --git a/apps/nextjs-app/yarn.lock b/apps/nextjs-app/yarn.lock index 8792d13e..e4bc6abd 100644 --- a/apps/nextjs-app/yarn.lock +++ b/apps/nextjs-app/yarn.lock @@ -8452,11 +8452,6 @@ netmask@^2.0.2: resolved "https://registry.yarnpkg.com/netmask/-/netmask-2.0.2.tgz#8b01a07644065d536383835823bc52004ebac5e7" integrity sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg== -next-router-mock@^0.9.13: - version "0.9.13" - resolved "https://registry.yarnpkg.com/next-router-mock/-/next-router-mock-0.9.13.tgz#bdee2011ea6c09e490121c354ef917f339767f72" - integrity sha512-906n2RRaE6Y28PfYJbaz5XZeJ6Tw8Xz1S6E31GGwZ0sXB6/XjldD1/2azn1ZmBmRk5PQRkzjg+n+RHZe5xQzWA== - next@^14.2.5: version "14.2.5" resolved "https://registry.yarnpkg.com/next/-/next-14.2.5.tgz#afe4022bb0b752962e2205836587a289270efbea" diff --git a/apps/nextjs-pages/index.html b/apps/nextjs-pages/index.html deleted file mode 100644 index 7c865abb..00000000 --- a/apps/nextjs-pages/index.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - Bulletproof React - - - -
- - -