Full Code of alan2207/bulletproof-react for AI

master c66ea0618e65 cached
511 files
699.2 KB
192.7k tokens
342 symbols
1 requests
Download .txt
Showing preview only (823K chars total). Download the full file or copy to clipboard to get everything.
Repository: alan2207/bulletproof-react
Branch: master
Commit: c66ea0618e65
Files: 511
Total size: 699.2 KB

Directory structure:
gitextract_dmcys4tl/

├── .github/
│   └── workflows/
│       ├── nextjs-app-ci.yml
│       ├── nextjs-pages-ci.yml
│       └── react-vite-ci.yml
├── .gitignore
├── .husky/
│   └── pre-commit
├── LICENSE
├── README.md
├── apps/
│   ├── nextjs-app/
│   │   ├── .eslintrc.cjs
│   │   ├── .gitignore
│   │   ├── .prettierignore
│   │   ├── .prettierrc
│   │   ├── .storybook/
│   │   │   ├── main.ts
│   │   │   └── preview.tsx
│   │   ├── .vscode/
│   │   │   ├── extensions.json
│   │   │   └── settings.json
│   │   ├── README.md
│   │   ├── __mocks__/
│   │   │   ├── vitest-env.d.ts
│   │   │   └── zustand.ts
│   │   ├── e2e/
│   │   │   ├── .eslintrc.cjs
│   │   │   └── tests/
│   │   │       ├── auth.setup.ts
│   │   │       ├── profile.spec.ts
│   │   │       └── smoke.spec.ts
│   │   ├── generators/
│   │   │   └── component/
│   │   │       ├── component.stories.tsx.hbs
│   │   │       ├── component.tsx.hbs
│   │   │       ├── index.cjs
│   │   │       └── index.ts.hbs
│   │   ├── index.html
│   │   ├── lint-staged.config.mjs
│   │   ├── mock-server.ts
│   │   ├── next-env.d.ts
│   │   ├── next.config.mjs
│   │   ├── package.json
│   │   ├── playwright.config.ts
│   │   ├── plopfile.cjs
│   │   ├── postcss.config.cjs
│   │   ├── public/
│   │   │   ├── _redirects
│   │   │   ├── mockServiceWorker.js
│   │   │   └── robots.txt
│   │   ├── src/
│   │   │   ├── app/
│   │   │   │   ├── app/
│   │   │   │   │   ├── _components/
│   │   │   │   │   │   ├── dashboard-info.tsx
│   │   │   │   │   │   └── dashboard-layout.tsx
│   │   │   │   │   ├── discussions/
│   │   │   │   │   │   ├── [discussionId]/
│   │   │   │   │   │   │   ├── __tests__/
│   │   │   │   │   │   │   │   └── discussion.test.tsx
│   │   │   │   │   │   │   ├── _components/
│   │   │   │   │   │   │   │   └── discussion.tsx
│   │   │   │   │   │   │   └── page.tsx
│   │   │   │   │   │   ├── __tests__/
│   │   │   │   │   │   │   └── discussions.test.tsx
│   │   │   │   │   │   ├── _components/
│   │   │   │   │   │   │   └── discussions.tsx
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   ├── layout.tsx
│   │   │   │   │   ├── page.tsx
│   │   │   │   │   ├── profile/
│   │   │   │   │   │   ├── _components/
│   │   │   │   │   │   │   └── profile.tsx
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   └── users/
│   │   │   │   │       ├── _components/
│   │   │   │   │       │   ├── admin-guard.tsx
│   │   │   │   │       │   └── users.tsx
│   │   │   │   │       └── page.tsx
│   │   │   │   ├── auth/
│   │   │   │   │   ├── _components/
│   │   │   │   │   │   └── auth-layout.tsx
│   │   │   │   │   ├── layout.tsx
│   │   │   │   │   ├── login/
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   └── register/
│   │   │   │   │       └── page.tsx
│   │   │   │   ├── layout.tsx
│   │   │   │   ├── not-found.tsx
│   │   │   │   ├── page.tsx
│   │   │   │   ├── provider.tsx
│   │   │   │   └── public/
│   │   │   │       └── discussions/
│   │   │   │           └── [discussionId]/
│   │   │   │               └── page.tsx
│   │   │   ├── components/
│   │   │   │   ├── errors/
│   │   │   │   │   └── main.tsx
│   │   │   │   ├── layouts/
│   │   │   │   │   └── content-layout.tsx
│   │   │   │   └── ui/
│   │   │   │       ├── button/
│   │   │   │       │   ├── button.stories.tsx
│   │   │   │       │   ├── button.tsx
│   │   │   │       │   └── index.ts
│   │   │   │       ├── dialog/
│   │   │   │       │   ├── __tests__/
│   │   │   │       │   │   └── dialog.test.tsx
│   │   │   │       │   ├── confirmation-dialog/
│   │   │   │       │   │   ├── __tests__/
│   │   │   │       │   │   │   └── confirmation-dialog.test.tsx
│   │   │   │       │   │   ├── confirmation-dialog.stories.tsx
│   │   │   │       │   │   ├── confirmation-dialog.tsx
│   │   │   │       │   │   └── index.ts
│   │   │   │       │   ├── dialog.stories.tsx
│   │   │   │       │   ├── dialog.tsx
│   │   │   │       │   └── index.ts
│   │   │   │       ├── drawer/
│   │   │   │       │   ├── __tests__/
│   │   │   │       │   │   └── drawer.test.tsx
│   │   │   │       │   ├── drawer.stories.tsx
│   │   │   │       │   ├── drawer.tsx
│   │   │   │       │   └── index.ts
│   │   │   │       ├── dropdown/
│   │   │   │       │   ├── dropdown.stories.tsx
│   │   │   │       │   ├── dropdown.tsx
│   │   │   │       │   └── index.ts
│   │   │   │       ├── form/
│   │   │   │       │   ├── __tests__/
│   │   │   │       │   │   └── form.test.tsx
│   │   │   │       │   ├── error.tsx
│   │   │   │       │   ├── field-wrapper.tsx
│   │   │   │       │   ├── form-drawer.tsx
│   │   │   │       │   ├── form.stories.tsx
│   │   │   │       │   ├── form.tsx
│   │   │   │       │   ├── index.ts
│   │   │   │       │   ├── input.tsx
│   │   │   │       │   ├── label.tsx
│   │   │   │       │   ├── select.tsx
│   │   │   │       │   ├── switch.tsx
│   │   │   │       │   └── textarea.tsx
│   │   │   │       ├── link/
│   │   │   │       │   ├── index.ts
│   │   │   │       │   ├── link.stories.tsx
│   │   │   │       │   └── link.tsx
│   │   │   │       ├── md-preview/
│   │   │   │       │   ├── index.ts
│   │   │   │       │   ├── md-preview.stories.tsx
│   │   │   │       │   └── md-preview.tsx
│   │   │   │       ├── notifications/
│   │   │   │       │   ├── __tests__/
│   │   │   │       │   │   └── notifications.test.ts
│   │   │   │       │   ├── index.ts
│   │   │   │       │   ├── notification.stories.tsx
│   │   │   │       │   ├── notification.tsx
│   │   │   │       │   ├── notifications-store.ts
│   │   │   │       │   └── notifications.tsx
│   │   │   │       ├── spinner/
│   │   │   │       │   ├── index.ts
│   │   │   │       │   ├── spinner.stories.tsx
│   │   │   │       │   └── spinner.tsx
│   │   │   │       └── table/
│   │   │   │           ├── index.ts
│   │   │   │           ├── pagination.tsx
│   │   │   │           ├── table.stories.tsx
│   │   │   │           └── table.tsx
│   │   │   ├── config/
│   │   │   │   ├── env.ts
│   │   │   │   └── paths.ts
│   │   │   ├── features/
│   │   │   │   ├── auth/
│   │   │   │   │   └── components/
│   │   │   │   │       ├── __tests__/
│   │   │   │   │       │   ├── login-form.test.tsx
│   │   │   │   │       │   └── register-form.test.tsx
│   │   │   │   │       ├── login-form.tsx
│   │   │   │   │       └── register-form.tsx
│   │   │   │   ├── comments/
│   │   │   │   │   ├── api/
│   │   │   │   │   │   ├── create-comment.ts
│   │   │   │   │   │   ├── delete-comment.ts
│   │   │   │   │   │   └── get-comments.ts
│   │   │   │   │   └── components/
│   │   │   │   │       ├── comments-list.tsx
│   │   │   │   │       ├── comments.tsx
│   │   │   │   │       ├── create-comment.tsx
│   │   │   │   │       └── delete-comment.tsx
│   │   │   │   ├── discussions/
│   │   │   │   │   ├── api/
│   │   │   │   │   │   ├── create-discussion.ts
│   │   │   │   │   │   ├── delete-discussion.ts
│   │   │   │   │   │   ├── get-discussion.ts
│   │   │   │   │   │   ├── get-discussions.ts
│   │   │   │   │   │   └── update-discussion.ts
│   │   │   │   │   └── components/
│   │   │   │   │       ├── create-discussion.tsx
│   │   │   │   │       ├── delete-discussion.tsx
│   │   │   │   │       ├── discussion-view.tsx
│   │   │   │   │       ├── discussions-list.tsx
│   │   │   │   │       └── update-discussion.tsx
│   │   │   │   ├── teams/
│   │   │   │   │   └── api/
│   │   │   │   │       └── get-teams.ts
│   │   │   │   └── users/
│   │   │   │       ├── api/
│   │   │   │       │   ├── delete-user.ts
│   │   │   │       │   ├── get-users.ts
│   │   │   │       │   └── update-profile.ts
│   │   │   │       └── components/
│   │   │   │           ├── delete-user.tsx
│   │   │   │           ├── update-profile.tsx
│   │   │   │           └── users-list.tsx
│   │   │   ├── hooks/
│   │   │   │   ├── __tests__/
│   │   │   │   │   └── use-disclosure.test.ts
│   │   │   │   └── use-disclosure.ts
│   │   │   ├── lib/
│   │   │   │   ├── __tests__/
│   │   │   │   │   └── authorization.test.tsx
│   │   │   │   ├── api-client.ts
│   │   │   │   ├── auth.tsx
│   │   │   │   ├── authorization.ts
│   │   │   │   └── react-query.ts
│   │   │   ├── styles/
│   │   │   │   └── globals.css
│   │   │   ├── testing/
│   │   │   │   ├── data-generators.ts
│   │   │   │   ├── mocks/
│   │   │   │   │   ├── browser.ts
│   │   │   │   │   ├── db.ts
│   │   │   │   │   ├── handlers/
│   │   │   │   │   │   ├── auth.ts
│   │   │   │   │   │   ├── comments.ts
│   │   │   │   │   │   ├── discussions.ts
│   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   ├── teams.ts
│   │   │   │   │   │   └── users.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── server.ts
│   │   │   │   │   └── utils.ts
│   │   │   │   ├── setup-tests.ts
│   │   │   │   └── test-utils.tsx
│   │   │   ├── types/
│   │   │   │   └── api.ts
│   │   │   └── utils/
│   │   │       ├── auth.ts
│   │   │       ├── cn.ts
│   │   │       └── format.ts
│   │   ├── tailwind.config.cjs
│   │   ├── tsconfig.json
│   │   └── vitest.config.ts
│   ├── nextjs-pages/
│   │   ├── .eslintrc.cjs
│   │   ├── .gitignore
│   │   ├── .prettierignore
│   │   ├── .prettierrc
│   │   ├── .storybook/
│   │   │   ├── main.ts
│   │   │   └── preview.tsx
│   │   ├── .vscode/
│   │   │   ├── extensions.json
│   │   │   └── settings.json
│   │   ├── README.md
│   │   ├── __mocks__/
│   │   │   ├── vitest-env.d.ts
│   │   │   └── zustand.ts
│   │   ├── e2e/
│   │   │   ├── .eslintrc.cjs
│   │   │   └── tests/
│   │   │       ├── auth.setup.ts
│   │   │       ├── profile.spec.ts
│   │   │       └── smoke.spec.ts
│   │   ├── generators/
│   │   │   └── component/
│   │   │       ├── component.stories.tsx.hbs
│   │   │       ├── component.tsx.hbs
│   │   │       ├── index.cjs
│   │   │       └── index.ts.hbs
│   │   ├── lint-staged.config.mjs
│   │   ├── mock-server.ts
│   │   ├── next-env.d.ts
│   │   ├── next.config.mjs
│   │   ├── package.json
│   │   ├── playwright.config.ts
│   │   ├── plopfile.cjs
│   │   ├── postcss.config.cjs
│   │   ├── public/
│   │   │   ├── _redirects
│   │   │   ├── mockServiceWorker.js
│   │   │   └── robots.txt
│   │   ├── src/
│   │   │   ├── app/
│   │   │   │   ├── pages/
│   │   │   │   │   ├── app/
│   │   │   │   │   │   ├── dashboard.tsx
│   │   │   │   │   │   ├── discussions/
│   │   │   │   │   │   │   ├── __tests__/
│   │   │   │   │   │   │   │   ├── discussion.test.tsx
│   │   │   │   │   │   │   │   └── discussions.test.tsx
│   │   │   │   │   │   │   ├── discussion.tsx
│   │   │   │   │   │   │   └── discussions.tsx
│   │   │   │   │   │   ├── profile.tsx
│   │   │   │   │   │   └── users.tsx
│   │   │   │   │   └── auth/
│   │   │   │   │       ├── login.tsx
│   │   │   │   │       └── register.tsx
│   │   │   │   └── provider.tsx
│   │   │   ├── components/
│   │   │   │   ├── errors/
│   │   │   │   │   └── main.tsx
│   │   │   │   ├── layouts/
│   │   │   │   │   ├── auth-layout.tsx
│   │   │   │   │   ├── content-layout.tsx
│   │   │   │   │   ├── dashboard-layout.tsx
│   │   │   │   │   └── index.ts
│   │   │   │   ├── seo/
│   │   │   │   │   ├── head.tsx
│   │   │   │   │   └── index.ts
│   │   │   │   └── ui/
│   │   │   │       ├── button/
│   │   │   │       │   ├── button.stories.tsx
│   │   │   │       │   ├── button.tsx
│   │   │   │       │   └── index.ts
│   │   │   │       ├── dialog/
│   │   │   │       │   ├── __tests__/
│   │   │   │       │   │   └── dialog.test.tsx
│   │   │   │       │   ├── confirmation-dialog/
│   │   │   │       │   │   ├── __tests__/
│   │   │   │       │   │   │   └── confirmation-dialog.test.tsx
│   │   │   │       │   │   ├── confirmation-dialog.stories.tsx
│   │   │   │       │   │   ├── confirmation-dialog.tsx
│   │   │   │       │   │   └── index.ts
│   │   │   │       │   ├── dialog.stories.tsx
│   │   │   │       │   ├── dialog.tsx
│   │   │   │       │   └── index.ts
│   │   │   │       ├── drawer/
│   │   │   │       │   ├── __tests__/
│   │   │   │       │   │   └── drawer.test.tsx
│   │   │   │       │   ├── drawer.stories.tsx
│   │   │   │       │   ├── drawer.tsx
│   │   │   │       │   └── index.ts
│   │   │   │       ├── dropdown/
│   │   │   │       │   ├── dropdown.stories.tsx
│   │   │   │       │   ├── dropdown.tsx
│   │   │   │       │   └── index.ts
│   │   │   │       ├── form/
│   │   │   │       │   ├── __tests__/
│   │   │   │       │   │   └── form.test.tsx
│   │   │   │       │   ├── error.tsx
│   │   │   │       │   ├── field-wrapper.tsx
│   │   │   │       │   ├── form-drawer.tsx
│   │   │   │       │   ├── form.stories.tsx
│   │   │   │       │   ├── form.tsx
│   │   │   │       │   ├── index.ts
│   │   │   │       │   ├── input.tsx
│   │   │   │       │   ├── label.tsx
│   │   │   │       │   ├── select.tsx
│   │   │   │       │   ├── switch.tsx
│   │   │   │       │   └── textarea.tsx
│   │   │   │       ├── link/
│   │   │   │       │   ├── index.ts
│   │   │   │       │   ├── link.stories.tsx
│   │   │   │       │   └── link.tsx
│   │   │   │       ├── md-preview/
│   │   │   │       │   ├── index.ts
│   │   │   │       │   ├── md-preview.stories.tsx
│   │   │   │       │   └── md-preview.tsx
│   │   │   │       ├── notifications/
│   │   │   │       │   ├── __tests__/
│   │   │   │       │   │   └── notifications.test.ts
│   │   │   │       │   ├── index.ts
│   │   │   │       │   ├── notification.stories.tsx
│   │   │   │       │   ├── notification.tsx
│   │   │   │       │   ├── notifications-store.ts
│   │   │   │       │   └── notifications.tsx
│   │   │   │       ├── spinner/
│   │   │   │       │   ├── index.ts
│   │   │   │       │   ├── spinner.stories.tsx
│   │   │   │       │   └── spinner.tsx
│   │   │   │       └── table/
│   │   │   │           ├── index.ts
│   │   │   │           ├── pagination.tsx
│   │   │   │           ├── table.stories.tsx
│   │   │   │           └── table.tsx
│   │   │   ├── config/
│   │   │   │   ├── env.ts
│   │   │   │   └── paths.ts
│   │   │   ├── features/
│   │   │   │   ├── auth/
│   │   │   │   │   └── components/
│   │   │   │   │       ├── __tests__/
│   │   │   │   │       │   ├── login-form.test.tsx
│   │   │   │   │       │   └── register-form.test.tsx
│   │   │   │   │       ├── login-form.tsx
│   │   │   │   │       └── register-form.tsx
│   │   │   │   ├── comments/
│   │   │   │   │   ├── api/
│   │   │   │   │   │   ├── create-comment.ts
│   │   │   │   │   │   ├── delete-comment.ts
│   │   │   │   │   │   └── get-comments.ts
│   │   │   │   │   └── components/
│   │   │   │   │       ├── comments-list.tsx
│   │   │   │   │       ├── comments.tsx
│   │   │   │   │       ├── create-comment.tsx
│   │   │   │   │       └── delete-comment.tsx
│   │   │   │   ├── discussions/
│   │   │   │   │   ├── api/
│   │   │   │   │   │   ├── create-discussion.ts
│   │   │   │   │   │   ├── delete-discussion.ts
│   │   │   │   │   │   ├── get-discussion.ts
│   │   │   │   │   │   ├── get-discussions.ts
│   │   │   │   │   │   └── update-discussion.ts
│   │   │   │   │   └── components/
│   │   │   │   │       ├── create-discussion.tsx
│   │   │   │   │       ├── delete-discussion.tsx
│   │   │   │   │       ├── discussion-view.tsx
│   │   │   │   │       ├── discussions-list.tsx
│   │   │   │   │       └── update-discussion.tsx
│   │   │   │   ├── teams/
│   │   │   │   │   └── api/
│   │   │   │   │       └── get-teams.ts
│   │   │   │   └── users/
│   │   │   │       ├── api/
│   │   │   │       │   ├── delete-user.ts
│   │   │   │       │   ├── get-users.ts
│   │   │   │       │   └── update-profile.ts
│   │   │   │       └── components/
│   │   │   │           ├── delete-user.tsx
│   │   │   │           ├── update-profile.tsx
│   │   │   │           └── users-list.tsx
│   │   │   ├── hooks/
│   │   │   │   ├── __tests__/
│   │   │   │   │   └── use-disclosure.test.ts
│   │   │   │   └── use-disclosure.ts
│   │   │   ├── lib/
│   │   │   │   ├── __tests__/
│   │   │   │   │   └── authorization.test.tsx
│   │   │   │   ├── api-client.ts
│   │   │   │   ├── auth.tsx
│   │   │   │   ├── authorization.tsx
│   │   │   │   └── react-query.ts
│   │   │   ├── pages/
│   │   │   │   ├── 404.tsx
│   │   │   │   ├── _app.tsx
│   │   │   │   ├── app/
│   │   │   │   │   ├── discussions/
│   │   │   │   │   │   ├── [discussionId].tsx
│   │   │   │   │   │   └── index.tsx
│   │   │   │   │   ├── index.tsx
│   │   │   │   │   ├── profile.tsx
│   │   │   │   │   └── users.tsx
│   │   │   │   ├── auth/
│   │   │   │   │   ├── login.tsx
│   │   │   │   │   └── register.tsx
│   │   │   │   ├── index.tsx
│   │   │   │   └── public/
│   │   │   │       └── discussions/
│   │   │   │           └── [discussionId].tsx
│   │   │   ├── styles/
│   │   │   │   └── globals.css
│   │   │   ├── testing/
│   │   │   │   ├── data-generators.ts
│   │   │   │   ├── mocks/
│   │   │   │   │   ├── browser.ts
│   │   │   │   │   ├── db.ts
│   │   │   │   │   ├── handlers/
│   │   │   │   │   │   ├── auth.ts
│   │   │   │   │   │   ├── comments.ts
│   │   │   │   │   │   ├── discussions.ts
│   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   ├── teams.ts
│   │   │   │   │   │   └── users.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── server.ts
│   │   │   │   │   └── utils.ts
│   │   │   │   ├── setup-tests.ts
│   │   │   │   └── test-utils.tsx
│   │   │   ├── types/
│   │   │   │   └── api.ts
│   │   │   └── utils/
│   │   │       ├── cn.ts
│   │   │       └── format.ts
│   │   ├── tailwind.config.cjs
│   │   ├── tsconfig.json
│   │   └── vitest.config.ts
│   └── react-vite/
│       ├── .eslintrc.cjs
│       ├── .gitignore
│       ├── .prettierignore
│       ├── .prettierrc
│       ├── .storybook/
│       │   ├── main.ts
│       │   └── preview.tsx
│       ├── .vscode/
│       │   ├── extensions.json
│       │   └── settings.json
│       ├── README.md
│       ├── __mocks__/
│       │   ├── vitest-env.d.ts
│       │   └── zustand.ts
│       ├── e2e/
│       │   ├── .eslintrc.cjs
│       │   └── tests/
│       │       ├── auth.setup.ts
│       │       ├── profile.spec.ts
│       │       └── smoke.spec.ts
│       ├── generators/
│       │   └── component/
│       │       ├── component.stories.tsx.hbs
│       │       ├── component.tsx.hbs
│       │       ├── index.cjs
│       │       └── index.ts.hbs
│       ├── index.html
│       ├── mock-server.ts
│       ├── package.json
│       ├── playwright.config.ts
│       ├── plopfile.cjs
│       ├── postcss.config.cjs
│       ├── public/
│       │   ├── _redirects
│       │   ├── mockServiceWorker.js
│       │   └── robots.txt
│       ├── src/
│       │   ├── app/
│       │   │   ├── index.tsx
│       │   │   ├── provider.tsx
│       │   │   ├── router.tsx
│       │   │   └── routes/
│       │   │       ├── app/
│       │   │       │   ├── dashboard.tsx
│       │   │       │   ├── discussions/
│       │   │       │   │   ├── __tests__/
│       │   │       │   │   │   ├── discussion.test.tsx
│       │   │       │   │   │   └── discussions.test.tsx
│       │   │       │   │   ├── discussion.tsx
│       │   │       │   │   └── discussions.tsx
│       │   │       │   ├── profile.tsx
│       │   │       │   ├── root.tsx
│       │   │       │   └── users.tsx
│       │   │       ├── auth/
│       │   │       │   ├── login.tsx
│       │   │       │   └── register.tsx
│       │   │       ├── landing.tsx
│       │   │       └── not-found.tsx
│       │   ├── components/
│       │   │   ├── errors/
│       │   │   │   └── main.tsx
│       │   │   ├── layouts/
│       │   │   │   ├── auth-layout.tsx
│       │   │   │   ├── content-layout.tsx
│       │   │   │   ├── dashboard-layout.tsx
│       │   │   │   └── index.ts
│       │   │   ├── seo/
│       │   │   │   ├── __tests__/
│       │   │   │   │   └── head.test.tsx
│       │   │   │   ├── head.tsx
│       │   │   │   └── index.ts
│       │   │   └── ui/
│       │   │       ├── button/
│       │   │       │   ├── button.stories.tsx
│       │   │       │   ├── button.tsx
│       │   │       │   └── index.ts
│       │   │       ├── dialog/
│       │   │       │   ├── __tests__/
│       │   │       │   │   └── dialog.test.tsx
│       │   │       │   ├── confirmation-dialog/
│       │   │       │   │   ├── __tests__/
│       │   │       │   │   │   └── confirmation-dialog.test.tsx
│       │   │       │   │   ├── confirmation-dialog.stories.tsx
│       │   │       │   │   ├── confirmation-dialog.tsx
│       │   │       │   │   └── index.ts
│       │   │       │   ├── dialog.stories.tsx
│       │   │       │   ├── dialog.tsx
│       │   │       │   └── index.ts
│       │   │       ├── drawer/
│       │   │       │   ├── __tests__/
│       │   │       │   │   └── drawer.test.tsx
│       │   │       │   ├── drawer.stories.tsx
│       │   │       │   ├── drawer.tsx
│       │   │       │   └── index.ts
│       │   │       ├── dropdown/
│       │   │       │   ├── dropdown.stories.tsx
│       │   │       │   ├── dropdown.tsx
│       │   │       │   └── index.ts
│       │   │       ├── form/
│       │   │       │   ├── __tests__/
│       │   │       │   │   └── form.test.tsx
│       │   │       │   ├── error.tsx
│       │   │       │   ├── field-wrapper.tsx
│       │   │       │   ├── form-drawer.tsx
│       │   │       │   ├── form.stories.tsx
│       │   │       │   ├── form.tsx
│       │   │       │   ├── index.ts
│       │   │       │   ├── input.tsx
│       │   │       │   ├── label.tsx
│       │   │       │   ├── select.tsx
│       │   │       │   ├── switch.tsx
│       │   │       │   └── textarea.tsx
│       │   │       ├── link/
│       │   │       │   ├── index.ts
│       │   │       │   ├── link.stories.tsx
│       │   │       │   └── link.tsx
│       │   │       ├── md-preview/
│       │   │       │   ├── index.ts
│       │   │       │   ├── md-preview.stories.tsx
│       │   │       │   └── md-preview.tsx
│       │   │       ├── notifications/
│       │   │       │   ├── __tests__/
│       │   │       │   │   └── notifications.test.ts
│       │   │       │   ├── index.ts
│       │   │       │   ├── notification.stories.tsx
│       │   │       │   ├── notification.tsx
│       │   │       │   ├── notifications-store.ts
│       │   │       │   └── notifications.tsx
│       │   │       ├── spinner/
│       │   │       │   ├── index.ts
│       │   │       │   ├── spinner.stories.tsx
│       │   │       │   └── spinner.tsx
│       │   │       └── table/
│       │   │           ├── index.ts
│       │   │           ├── pagination.tsx
│       │   │           ├── table.stories.tsx
│       │   │           └── table.tsx
│       │   ├── config/
│       │   │   ├── env.ts
│       │   │   └── paths.ts
│       │   ├── features/
│       │   │   ├── auth/
│       │   │   │   └── components/
│       │   │   │       ├── __tests__/
│       │   │   │       │   ├── login-form.test.tsx
│       │   │   │       │   └── register-form.test.tsx
│       │   │   │       ├── login-form.tsx
│       │   │   │       └── register-form.tsx
│       │   │   ├── comments/
│       │   │   │   ├── api/
│       │   │   │   │   ├── create-comment.ts
│       │   │   │   │   ├── delete-comment.ts
│       │   │   │   │   └── get-comments.ts
│       │   │   │   └── components/
│       │   │   │       ├── comments-list.tsx
│       │   │   │       ├── comments.tsx
│       │   │   │       ├── create-comment.tsx
│       │   │   │       └── delete-comment.tsx
│       │   │   ├── discussions/
│       │   │   │   ├── api/
│       │   │   │   │   ├── create-discussion.ts
│       │   │   │   │   ├── delete-discussion.ts
│       │   │   │   │   ├── get-discussion.ts
│       │   │   │   │   ├── get-discussions.ts
│       │   │   │   │   └── update-discussion.ts
│       │   │   │   └── components/
│       │   │   │       ├── create-discussion.tsx
│       │   │   │       ├── delete-discussion.tsx
│       │   │   │       ├── discussion-view.tsx
│       │   │   │       ├── discussions-list.tsx
│       │   │   │       └── update-discussion.tsx
│       │   │   ├── teams/
│       │   │   │   └── api/
│       │   │   │       └── get-teams.ts
│       │   │   └── users/
│       │   │       ├── api/
│       │   │       │   ├── delete-user.ts
│       │   │       │   ├── get-users.ts
│       │   │       │   └── update-profile.ts
│       │   │       └── components/
│       │   │           ├── delete-user.tsx
│       │   │           ├── update-profile.tsx
│       │   │           └── users-list.tsx
│       │   ├── hooks/
│       │   │   ├── __tests__/
│       │   │   │   └── use-disclosure.test.ts
│       │   │   └── use-disclosure.ts
│       │   ├── index.css
│       │   ├── lib/
│       │   │   ├── __tests__/
│       │   │   │   └── authorization.test.tsx
│       │   │   ├── api-client.ts
│       │   │   ├── auth.tsx
│       │   │   ├── authorization.tsx
│       │   │   └── react-query.ts
│       │   ├── main.tsx
│       │   ├── testing/
│       │   │   ├── data-generators.ts
│       │   │   ├── mocks/
│       │   │   │   ├── browser.ts
│       │   │   │   ├── db.ts
│       │   │   │   ├── handlers/
│       │   │   │   │   ├── auth.ts
│       │   │   │   │   ├── comments.ts
│       │   │   │   │   ├── discussions.ts
│       │   │   │   │   ├── index.ts
│       │   │   │   │   ├── teams.ts
│       │   │   │   │   └── users.ts
│       │   │   │   ├── index.ts
│       │   │   │   ├── server.ts
│       │   │   │   └── utils.ts
│       │   │   ├── setup-tests.ts
│       │   │   └── test-utils.tsx
│       │   ├── types/
│       │   │   └── api.ts
│       │   ├── utils/
│       │   │   ├── cn.ts
│       │   │   └── format.ts
│       │   └── vite-env.d.ts
│       ├── tailwind.config.cjs
│       ├── tsconfig.json
│       ├── vite-env.d.ts
│       └── vite.config.ts
├── docs/
│   ├── additional-resources.md
│   ├── api-layer.md
│   ├── application-overview.md
│   ├── components-and-styling.md
│   ├── deployment.md
│   ├── error-handling.md
│   ├── performance.md
│   ├── project-standards.md
│   ├── project-structure.md
│   ├── security.md
│   ├── state-management.md
│   └── testing.md
└── package.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/nextjs-app-ci.yml
================================================
name: Next.js App CI
on:
  push:
    branches: ["*"]
    paths-ignore:
      - "README.md"
      - "docs/**"
  pull_request:
    branches: [master]
    paths-ignore:
      - "README.md"
      - "docs/**"
jobs:
  all-cli-checks:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ./apps/nextjs-app
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: lts/*
      - name: Set environment variables
        run: mv .env.example .env
      - name: Install dependencies
        run: yarn install
      - name: Build application
        run: yarn build
      - name: Run tests
        run: yarn test
      - name: Run linter
        run: yarn lint
      - name: Check types
        run: yarn check-types
  e2e:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ./apps/nextjs-app
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: lts/*
      - name: Set environment variables
        run: mv .env.example-e2e .env
      - name: Install dependencies
        run: npm install -g yarn && yarn
      - name: Install Playwright Browsers
        run: yarn playwright install --with-deps
      - name: Run Playwright tests
        run: yarn test-e2e
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: playwright-report
          path: |
            playwright-report/
            mocked-db.json
          retention-days: 30


================================================
FILE: .github/workflows/nextjs-pages-ci.yml
================================================
name: Next.js Pages CI
on:
  push:
    branches: ["*"]
    paths-ignore:
      - "README.md"
      - "docs/**"
  pull_request:
    branches: [master]
    paths-ignore:
      - "README.md"
      - "docs/**"
jobs:
  all-cli-checks:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ./apps/nextjs-pages
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: lts/*
      - name: Set environment variables
        run: mv .env.example .env
      - name: Install dependencies
        run: yarn install
      - name: Build application
        run: yarn build
      - name: Run tests
        run: yarn test
      - name: Run linter
        run: yarn lint
      - name: Check types
        run: yarn check-types
  e2e:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ./apps/nextjs-pages
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: lts/*
      - name: Set environment variables
        run: mv .env.example-e2e .env
      - name: Install dependencies
        run: npm install -g yarn && yarn
      - name: Install Playwright Browsers
        run: yarn playwright install --with-deps
      - name: Run Playwright tests
        run: yarn test-e2e
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: playwright-report
          path: |
            playwright-report/
            mocked-db.json
          retention-days: 30


================================================
FILE: .github/workflows/react-vite-ci.yml
================================================
name: React Vite CI
on:
  push:
    branches: ["*"]
    paths-ignore:
      - "README.md"
      - "docs/**"
  pull_request:
    branches: [master]
    paths-ignore:
      - "README.md"
      - "docs/**"
jobs:
  all-cli-checks:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ./apps/react-vite
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: lts/*
      - name: Set environment variables
        run: mv .env.example .env
      - name: Install dependencies
        run: yarn install
      - name: Build application
        run: yarn build
      - name: Run tests
        run: yarn test
      - name: Run linter
        run: yarn lint
      - name: Check types
        run: yarn check-types
  e2e:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ./apps/react-vite
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: lts/*
      - name: Set environment variables
        run: mv .env.example-e2e .env
      - name: Install dependencies
        run: npm install -g yarn && yarn
      - name: Install Playwright Browsers
        run: yarn playwright install --with-deps
      - name: Run Playwright tests
        run: yarn test-e2e
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: playwright-report
          path: |
            playwright-report/
            mocked-db.json
          retention-days: 30


================================================
FILE: .gitignore
================================================
# dependencies
/node_modules
/.pnp
.pnp.js

# misc
.DS_Store

================================================
FILE: .husky/pre-commit
================================================
yarn --cwd apps/nextjs-app lint-staged && yarn --cwd apps/nextjs-pages lint-staged && yarn --cwd apps/react-vite lint-staged

================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2024 Alan Alickovic

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# Bulletproof React 🛡️ ⚛️

[![MIT License](https://img.shields.io/github/license/alan2207/bulletproof-react)](https://github.com/alan2207/bulletproof-react/blob/master/LICENSE)
[![Next.js App CI](https://github.com/alan2207/bulletproof-react/actions/workflows/nextjs-app-ci.yml/badge.svg)](https://github.com/alan2207/bulletproof-react/actions/workflows/nextjs-app-ci.yml)
[![Next.js Pages CI](https://github.com/alan2207/bulletproof-react/actions/workflows/nextjs-pages-ci.yml/badge.svg)](https://github.com/alan2207/bulletproof-react/actions/workflows/nextjs-pages-ci.yml)
[![React Vite CI](https://github.com/alan2207/bulletproof-react/actions/workflows/react-vite-ci.yml/badge.svg)](https://github.com/alan2207/bulletproof-react/actions/workflows/react-vite-ci.yml)

A simple, scalable, and powerful architecture for building production ready React applications.

## Introduction

React is an excellent tool for building front-end applications. It has a diverse ecosystem with hundreds of great libraries for literally anything you might need. However, being forced to make so many choices can be overwhelming. It is also very flexible, you can write React applications in any way you like, but that flexibility comes with a cost. Since there is no pre-defined architecture that developers can follow, it often leads to a messy, inconsistent, and over-complicated codebase.

This repo attempts to present a way of creating React applications using some of the best tools in the ecosystem with a good project structure that scales very well. Based on my experience working with a lot of different codebases, this architecture turns out to be the most effective.

The goal here is to serve as a collection of resources and best practices when developing React applications. It is supposed to showcase solving most of the real-world problems of an application in a practical way and help developers write better applications.

Feel free to explore the sample app codebase to get the most value out of the repo.

> 🤝 **Looking for help implementing these patterns at your company?** [Get in touch](mailto:alan2207@live.com)

## What makes a React application "bulletproof"?

This repo doesn't aim to be a silver bullet for all React applications as there are many different use cases, but it tries to provide a solid foundation for building applications based on the following principles:

- Easy to get started with
- Simple to understand and maintain
- Uses the right tools for the job
- Clean boundaries between different parts of the application
- Everyone on the team is on the same page when it comes to how things are done
- Secure
- Performant
- Scalable in terms of codebase and team size
- Issues detectable as early as possible

#### Disclaimer:

This is not supposed to be a template, boilerplate or a framework. It is an opinionated guide that shows how to do some things in a certain way. You are not forced to do everything exactly as it is shown here, decide what works best for you and your team and stay consistent with your style.

To get most out of it, do not get limited by the technologies used in this sample app, but rather focus on the principles and the concepts that are being presented here. The tools and libraries used here are just a suggestion, you can always replace them with something that fits your needs better. Sometimes, your project might require a slightly different approach, and that's totally fine.

## Table Of Contents:

- [💻 Application Overview](docs/application-overview.md)
- [⚙️ Project Standards](docs/project-standards.md)
- [🗄️ Project Structure](docs/project-structure.md)
- [🧱 Components And Styling](docs/components-and-styling.md)
- [📡 API Layer](docs/api-layer.md)
- [🗃️ State Management](docs/state-management.md)
- [🧪 Testing](docs/testing.md)
- [⚠️ Error Handling](docs/error-handling.md)
- [🔐 Security](docs/security.md)
- [🚄 Performance](docs/performance.md)
- [🌐 Deployment](docs/deployment.md)
- [📚 Additional Resources](docs/additional-resources.md)

## Contributing

Contributions are always welcome! If you have any ideas, suggestions, fixes, feel free to contribute. You can do that by going through the following steps:

1. Clone this repo
2. Create a branch: `git checkout -b your-feature`
3. Execute the `yarn prepare` script.
4. Make some changes
5. Test your changes
6. Push your branch and open a Pull Request

## License

[MIT](/LICENSE)


================================================
FILE: apps/nextjs-app/.eslintrc.cjs
================================================
module.exports = {
  root: true,
  env: {
    node: true,
    es6: true,
  },
  parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
  ignorePatterns: [
    'node_modules/*',
    'public/mockServiceWorker.js',
    'generators/*',
  ],
  extends: ['eslint:recommended', 'next/core-web-vitals'],
  overrides: [
    {
      files: ['**/*.ts', '**/*.tsx'],
      parser: '@typescript-eslint/parser',
      settings: {
        react: { version: 'detect' },
        'import/resolver': {
          typescript: {},
        },
      },
      env: {
        browser: true,
        node: true,
        es6: true,
      },
      extends: [
        'eslint:recommended',
        'plugin:import/errors',
        'plugin:import/warnings',
        'plugin:import/typescript',
        'plugin:@typescript-eslint/recommended',
        'plugin:react/recommended',
        'plugin:react-hooks/recommended',
        'plugin:jsx-a11y/recommended',
        'plugin:prettier/recommended',
        'plugin:testing-library/react',
        'plugin:jest-dom/recommended',
        'plugin:tailwindcss/recommended',
        'plugin:vitest/legacy-recommended',
      ],
      rules: {
        '@next/next/no-img-element': 'off',
        'import/no-restricted-paths': [
          'error',
          {
            zones: [
              // disables cross-feature imports:
              // eg. src/features/discussions should not import from src/features/comments, etc.
              {
                target: './src/features/auth',
                from: './src/features',
                except: ['./auth'],
              },
              {
                target: './src/features/comments',
                from: './src/features',
                except: ['./comments'],
              },
              {
                target: './src/features/discussions',
                from: './src/features',
                except: ['./discussions'],
              },
              {
                target: './src/features/teams',
                from: './src/features',
                except: ['./teams'],
              },
              {
                target: './src/features/users',
                from: './src/features',
                except: ['./users'],
              },
              // enforce unidirectional codebase:

              // e.g. src/app can import from src/features but not the other way around
              {
                target: './src/features',
                from: './src/app',
              },

              // e.g src/features and src/app can import from these shared modules but not the other way around
              {
                target: [
                  './src/components',
                  './src/hooks',
                  './src/lib',
                  './src/types',
                  './src/utils',
                ],
                from: ['./src/features', './src/app'],
              },
            ],
          },
        ],
        'import/no-cycle': 'error',
        'linebreak-style': ['error', 'unix'],
        'react/prop-types': 'off',
        'import/order': [
          'error',
          {
            groups: [
              'builtin',
              'external',
              'internal',
              'parent',
              'sibling',
              'index',
              'object',
            ],
            'newlines-between': 'always',
            alphabetize: { order: 'asc', caseInsensitive: true },
          },
        ],
        'import/default': 'off',
        'import/no-named-as-default-member': 'off',
        'import/no-named-as-default': 'off',
        'react/react-in-jsx-scope': 'off',
        'jsx-a11y/anchor-is-valid': 'off',
        '@typescript-eslint/no-unused-vars': ['error'],
        '@typescript-eslint/explicit-function-return-type': ['off'],
        '@typescript-eslint/explicit-module-boundary-types': ['off'],
        '@typescript-eslint/no-empty-function': ['off'],
        '@typescript-eslint/no-explicit-any': ['off'],
        'prettier/prettier': ['error', {}, { usePrettierrc: true }],
      },
    },
    {
      plugins: ['check-file'],
      files: ['src/**/*'],
      rules: {
        'check-file/filename-naming-convention': [
          'error',
          {
            '**/*.{ts,tsx}': 'KEBAB_CASE',
          },
          {
            ignoreMiddleExtensions: true,
          },
        ],
        'check-file/folder-naming-convention': [
          'error',
          {
            '!(src/app)/**/*': 'KEBAB_CASE',
            '!(**/__tests__)/**/*': 'KEBAB_CASE',
          },
        ],
      },
    },
  ],
};


================================================
FILE: apps/nextjs-app/.gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
/e2e/.auth/

# storybook
migration-storybook.log
storybook.log
storybook-static


# production
/dist

# misc
.DS_Store
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*


# local
mocked-db.json

/.next
/.vite
tsconfig.tsbuildinfo

================================================
FILE: apps/nextjs-app/.prettierignore
================================================
*.hbs

================================================
FILE: apps/nextjs-app/.prettierrc
================================================
{
  "singleQuote": true,
  "trailingComma": "all",
  "printWidth": 80,
  "tabWidth": 2,
  "useTabs": false
}


================================================
FILE: apps/nextjs-app/.storybook/main.ts
================================================
module.exports = {
  stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],

  addons: [
    '@storybook/addon-actions',
    '@storybook/addon-links',
    '@storybook/node-logger',
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
    '@storybook/addon-docs',
    '@storybook/addon-a11y',
  ],
  framework: '@storybook/nextjs',
  docs: {
    autodocs: 'tag',
  },
  typescript: {
    reactDocgen: 'react-docgen-typescript',
  },
};


================================================
FILE: apps/nextjs-app/.storybook/preview.tsx
================================================
import React from 'react';
import '../src/styles/globals.css';

export const parameters = {
  actions: { argTypesRegex: '^on[A-Z].*' },
};

export const decorators = [(Story) => <Story />];


================================================
FILE: apps/nextjs-app/.vscode/extensions.json
================================================
{
  "recommendations": [
    "dbaeumer.vscode-eslint",
    "esbenp.prettier-vscode",
    "dsznajder.es7-react-js-snippets",
    "mariusalchimavicius.json-to-ts",
    "bradlc.vscode-tailwindcss"
  ]
}


================================================
FILE: apps/nextjs-app/.vscode/settings.json
================================================
{
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit"
  }
}


================================================
FILE: apps/nextjs-app/README.md
================================================
# Next.js App Application

## Get Started

Prerequisites:

- Node 20+
- Yarn 1.22+

To set up the app execute the following commands.

```bash
git clone https://github.com/alan2207/bulletproof-react.git
cd bulletproof-react
cd apps/nextjs-app
cp .env.example .env
yarn install
```

#### `yarn run-mock-server`

Make sure to start the mock server before running the app.
The mock server runs on [http://localhost:8080/api](http://localhost:8080/api).

##### `yarn dev`

Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.


================================================
FILE: apps/nextjs-app/__mocks__/vitest-env.d.ts
================================================
/// <reference types="vite/client" />
/// <reference types="vitest/globals" />


================================================
FILE: apps/nextjs-app/__mocks__/zustand.ts
================================================
import { act } from '@testing-library/react';
import { afterEach, vi } from 'vitest';
import * as zustand from 'zustand';

const { create: actualCreate, createStore: actualCreateStore } =
  await vi.importActual<typeof zustand>('zustand');

// a variable to hold reset functions for all stores declared in the app
export const storeResetFns = new Set<() => void>();

const createUncurried = <T>(stateCreator: zustand.StateCreator<T>) => {
  const store = actualCreate(stateCreator);
  const initialState = store.getInitialState();
  storeResetFns.add(() => {
    store.setState(initialState, true);
  });
  return store;
};

// when creating a store, we get its initial state, create a reset function and add it in the set
export const create = (<T>(stateCreator: zustand.StateCreator<T>) => {
  // to support curried version of create
  return typeof stateCreator === 'function'
    ? createUncurried(stateCreator)
    : createUncurried;
}) as typeof zustand.create;

const createStoreUncurried = <T>(stateCreator: zustand.StateCreator<T>) => {
  const store = actualCreateStore(stateCreator);
  const initialState = store.getInitialState();
  storeResetFns.add(() => {
    store.setState(initialState, true);
  });
  return store;
};

// when creating a store, we get its initial state, create a reset function and add it in the set
export const createStore = (<T>(stateCreator: zustand.StateCreator<T>) => {
  // to support curried version of createStore
  return typeof stateCreator === 'function'
    ? createStoreUncurried(stateCreator)
    : createStoreUncurried;
}) as typeof zustand.createStore;

// reset all stores after each test run
afterEach(() => {
  act(() => {
    storeResetFns.forEach((resetFn) => {
      resetFn();
    });
  });
});


================================================
FILE: apps/nextjs-app/e2e/.eslintrc.cjs
================================================
module.exports = {
  root: true,
  parser: '@typescript-eslint/parser',
  extends: 'plugin:playwright/recommended',
};


================================================
FILE: apps/nextjs-app/e2e/tests/auth.setup.ts
================================================
import { test as setup, expect } from '@playwright/test';
import { createUser } from '../../src/testing/data-generators';

const authFile = 'e2e/.auth/user.json';

setup('authenticate', async ({ page }) => {
  const user = createUser();

  await page.goto('/');
  await page.getByRole('button', { name: 'Get started' }).click();
  await page.waitForURL('/auth/login');
  await page.getByRole('link', { name: 'Register' }).click();

  // registration:
  await page.getByLabel('First Name').click();
  await page.getByLabel('First Name').fill(user.firstName);
  await page.getByLabel('Last Name').click();
  await page.getByLabel('Last Name').fill(user.lastName);
  await page.getByLabel('Email Address').click();
  await page.getByLabel('Email Address').fill(user.email);
  await page.getByLabel('Password').click();
  await page.getByLabel('Password').fill(user.password);
  await page.getByLabel('Team Name').click();
  await page.getByLabel('Team Name').fill(user.teamName);
  await page.getByRole('button', { name: 'Register' }).click();
  await page.waitForURL('/app');

  // 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=%2Fapp');

  // log in:
  await page.getByLabel('Email Address').click();
  await page.getByLabel('Email Address').fill(user.email);
  await page.getByLabel('Password').click();
  await page.getByLabel('Password').fill(user.password);
  await page.getByRole('button', { name: 'Log in' }).click();
  await page.waitForURL('/app');

  await page.context().storageState({ path: authFile });
});


================================================
FILE: apps/nextjs-app/e2e/tests/profile.spec.ts
================================================
import { test, expect } from '@playwright/test';

test('profile', async ({ page }) => {
  // update user:
  await page.goto('/app');
  await page.getByRole('button', { name: 'Open user menu' }).click();
  await page.getByRole('menuitem', { name: 'Your Profile' }).click();
  await page.getByRole('button', { name: 'Update Profile' }).click();
  await page.getByLabel('Bio').click();
  await page.getByLabel('Bio').fill('My bio');
  await page.getByRole('button', { name: 'Submit' }).click();
  await page
    .getByLabel('Profile Updated')
    .getByRole('button', { name: 'Close' })
    .click();
  await expect(page.getByText('My bio')).toBeVisible();
});


================================================
FILE: apps/nextjs-app/e2e/tests/smoke.spec.ts
================================================
import { test, expect } from '@playwright/test';

import {
  createDiscussion,
  createComment,
} from '../../src/testing/data-generators';
test('smoke', async ({ page }) => {
  const discussion = createDiscussion();
  const comment = createComment();

  await page.goto('/');
  await page.getByRole('button', { name: 'Get started' }).click();
  await page.waitForURL('/app');

  // create discussion:
  await page.getByRole('link', { name: 'Discussions' }).click();
  await page.waitForURL('/app/discussions');

  await page.getByRole('button', { name: 'Create Discussion' }).click();
  await page.getByLabel('Title').click();
  await page.getByLabel('Title').fill(discussion.title);
  await page.getByLabel('Body').click();
  await page.getByLabel('Body').fill(discussion.body);
  await page.getByRole('button', { name: 'Submit' }).click();
  await page
    .getByLabel('Discussion Created')
    .getByRole('button', { name: 'Close' })
    .click();

  // visit discussion page:
  await page.getByRole('link', { name: 'View' }).click();

  await expect(
    page.getByRole('heading', { name: discussion.title }),
  ).toBeVisible();
  await expect(page.getByText(discussion.body)).toBeVisible();

  // update discussion:
  await page.getByRole('button', { name: 'Update Discussion' }).click();
  await page.getByLabel('Title').click();
  await page.getByLabel('Title').fill(`${discussion.title} - updated`);
  await page.getByLabel('Body').click();
  await page.getByLabel('Body').fill(`${discussion.body} - updated`);
  await page.getByRole('button', { name: 'Submit' }).click();
  await page
    .getByLabel('Discussion Updated')
    .getByRole('button', { name: 'Close' })
    .click();

  await expect(
    page.getByRole('heading', { name: `${discussion.title} - updated` }),
  ).toBeVisible();
  await expect(page.getByText(`${discussion.body} - updated`)).toBeVisible();

  // create comment:
  await page.getByRole('button', { name: 'Create Comment' }).click();
  await page.getByLabel('Body').click();
  await page.getByLabel('Body').fill(comment.body);
  await page.getByRole('button', { name: 'Submit' }).click();
  await expect(page.getByText(comment.body)).toBeVisible();
  await page
    .getByLabel('Comment Created')
    .getByRole('button', { name: 'Close' })
    .click();

  // delete comment:
  await page.getByRole('button', { name: 'Delete Comment' }).click();
  await expect(
    page.getByText('Are you sure you want to delete this comment?'),
  ).toBeVisible();
  await page.getByRole('button', { name: 'Delete Comment' }).click();
  await page
    .getByLabel('Comment Deleted')
    .getByRole('button', { name: 'Close' })
    .click();
  await expect(
    page.getByRole('heading', { name: 'No Comments Found' }),
  ).toBeVisible();
  await expect(page.getByText(comment.body)).toBeHidden();

  // go back to discussions:
  await page.getByRole('link', { name: 'Discussions' }).click();
  await page.waitForURL('/app/discussions');

  // delete discussion:
  await page.getByRole('button', { name: 'Delete Discussion' }).click();
  await page.getByRole('button', { name: 'Delete Discussion' }).click();
  await page
    .getByLabel('Discussion Deleted')
    .getByRole('button', { name: 'Close' })
    .click();
  await expect(
    page.getByRole('heading', { name: 'No Entries Found' }),
  ).toBeVisible();
});


================================================
FILE: apps/nextjs-app/generators/component/component.stories.tsx.hbs
================================================
import { Meta, StoryObj } from '@storybook/react';

import { {{ properCase name }} } from './{{ kebabCase name }}';

const meta: Meta<typeof {{ properCase name }}> = {
  component: {{ properCase name }},
};

export default meta;

type Story = StoryObj<typeof {{ properCase name }}>;

export const Default: Story = {
  args: {}
};


================================================
FILE: apps/nextjs-app/generators/component/component.tsx.hbs
================================================
import * as React from "react"; 

export type {{properCase name}}Props = {};

export const {{properCase name}} = (props: {{properCase name}}Props) => { 
  return (
    <div>
      {{properCase name}}
    </div>
  ); 
};

================================================
FILE: apps/nextjs-app/generators/component/index.cjs
================================================
const path = require('path');
const fs = require('fs');

const featuresDir = path.join(process.cwd(), 'src/features');
const features = fs.readdirSync(featuresDir);

/**
 *
 * @type {import('plop').PlopGenerator}
 */
module.exports = {
  description: 'Component Generator',
  prompts: [
    {
      type: 'input',
      name: 'name',
      message: 'component name',
    },
    {
      type: 'list',
      name: 'feature',
      message: 'Which feature does this component belong to?',
      choices: ['components', ...features],
      when: () => features.length > 0,
    },
    {
      type: 'input',
      name: 'folder',
      message: 'folder in components',
      when: ({ feature }) => !feature || feature === 'components',
    },
  ],
  actions: (answers) => {
    const componentGeneratePath =
      !answers.feature || answers.feature === 'components'
        ? 'src/components/{{folder}}'
        : 'src/features/{{feature}}/components';
    return [
      {
        type: 'add',
        path: componentGeneratePath + '/{{kebabCase name}}/index.ts',
        templateFile: 'generators/component/index.ts.hbs',
      },
      {
        type: 'add',
        path: componentGeneratePath + '/{{kebabCase name}}/{{kebabCase name}}.tsx',
        templateFile: 'generators/component/component.tsx.hbs',
      },
      {
        type: 'add',
        path: componentGeneratePath + '/{{kebabCase name}}/{{kebabCase name}}.stories.tsx',
        templateFile: 'generators/component/component.stories.tsx.hbs',
      },
    ];
  },
};


================================================
FILE: apps/nextjs-app/generators/component/index.ts.hbs
================================================
export * from './{{ kebabCase name }}';


================================================
FILE: apps/nextjs-app/index.html
================================================
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta name="description" content="Bulletproof React Application" />
    <link rel="stylesheet" href="https://rsms.me/inter/inter.css" />

    <title>Bulletproof React</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>


================================================
FILE: apps/nextjs-app/lint-staged.config.mjs
================================================
import path from 'path';

const buildEslintCommand = (filenames) => {
  return `next lint --fix --file ${filenames
    .filter((f) => f.includes('/src/'))
    .map((f) => path.relative(process.cwd(), f))
    .join(' --file ')}`;
};

const config = {
  '*.{ts,tsx}': [buildEslintCommand, "bash -c 'yarn check-types'"],
};

export default config;


================================================
FILE: apps/nextjs-app/mock-server.ts
================================================
import { createMiddleware } from '@mswjs/http-middleware';
import cors from 'cors';
import express from 'express';
import logger from 'pino-http';

import { initializeDb } from './src/testing/mocks/db';
import { handlers } from './src/testing/mocks/handlers';

const app = express();

app.use(
  cors({
    origin: process.env.NEXT_PUBLIC_URL,
    credentials: true,
  }),
);

app.use(express.json());
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(() => {
  console.log('Mock DB initialized');
  app.listen(process.env.NEXT_PUBLIC_MOCK_API_PORT, () => {
    console.log(
      `Mock API server started at http://localhost:${process.env.NEXT_PUBLIC_MOCK_API_PORT}`,
    );
  });
});


================================================
FILE: apps/nextjs-app/next-env.d.ts
================================================
/// <reference types="next" />
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.


================================================
FILE: apps/nextjs-app/next.config.mjs
================================================
/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
};

export default nextConfig;


================================================
FILE: apps/nextjs-app/package.json
================================================
{
  "name": "bulletproof-react-nextjs-pages",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "test": "vitest",
    "test-e2e": "pm2 start \"yarn run-mock-server\" --name server && yarn playwright test",
    "prepare": "husky",
    "check-types": "tsc --project tsconfig.json --pretty --noEmit",
    "generate": "plop",
    "storybook": "storybook dev -p 6006",
    "build-storybook": "storybook build",
    "run-mock-server": "tsx ./mock-server.ts"
  },
  "dependencies": {
    "@hookform/resolvers": "^3.3.4",
    "@next/env": "^14.2.5",
    "@ngneat/falso": "^7.2.0",
    "@radix-ui/react-dialog": "^1.0.5",
    "@radix-ui/react-dropdown-menu": "^2.0.6",
    "@radix-ui/react-icons": "^1.3.0",
    "@radix-ui/react-label": "^2.0.2",
    "@radix-ui/react-slot": "^1.0.2",
    "@radix-ui/react-switch": "^1.0.3",
    "@tanstack/react-query": "^5.32.0",
    "@tanstack/react-query-devtools": "^5.32.0",
    "class-variance-authority": "^0.7.0",
    "clsx": "^2.1.1",
    "dayjs": "^1.11.11",
    "eslint-plugin-check-file": "^2.8.0",
    "isomorphic-dompurify": "^2.14.0",
    "lucide-react": "^0.378.0",
    "marked": "^12.0.2",
    "nanoid": "^5.0.7",
    "next": "^14.2.5",
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "react-error-boundary": "^4.0.13",
    "react-hook-form": "^7.51.3",
    "tailwind-merge": "^2.3.0",
    "tailwindcss-animate": "^1.0.7",
    "zod": "^3.23.4",
    "zustand": "^4.5.2"
  },
  "devDependencies": {
    "@eslint/eslintrc": "^3.0.2",
    "@mswjs/data": "^0.16.1",
    "@mswjs/http-middleware": "^0.10.1",
    "@playwright/test": "^1.43.1",
    "@storybook/addon-a11y": "^8.0.10",
    "@storybook/addon-actions": "^8.0.9",
    "@storybook/addon-essentials": "^8.0.9",
    "@storybook/addon-links": "^8.0.9",
    "@storybook/nextjs": "^8.2.9",
    "@storybook/node-logger": "^8.0.9",
    "@storybook/react": "^8.0.9",
    "@tailwindcss/typography": "^0.5.13",
    "@testing-library/jest-dom": "^6.4.2",
    "@testing-library/react": "^15.0.5",
    "@testing-library/user-event": "^14.5.2",
    "@types/cors": "^2.8.17",
    "@types/dompurify": "^3.0.5",
    "@types/js-cookie": "^3.0.6",
    "@types/marked": "^6.0.0",
    "@types/node": "^20.12.7",
    "@types/react": "^18.3.1",
    "@types/react-dom": "^18.3.0",
    "@typescript-eslint/eslint-plugin": "^7.8.0",
    "@typescript-eslint/parser": "^7.8.0",
    "@vitejs/plugin-react": "^4.2.1",
    "autoprefixer": "^10.4.19",
    "cors": "^2.8.5",
    "dotenv": "^16.4.5",
    "eslint": "8",
    "eslint-config-next": "^14.2.5",
    "eslint-config-prettier": "^9.1.0",
    "eslint-import-resolver-typescript": "^3.6.1",
    "eslint-plugin-import": "^2.29.1",
    "eslint-plugin-jest-dom": "^5.4.0",
    "eslint-plugin-jsx-a11y": "^6.8.0",
    "eslint-plugin-playwright": "^1.6.0",
    "eslint-plugin-prettier": "^5.1.3",
    "eslint-plugin-react": "^7.34.1",
    "eslint-plugin-react-hooks": "^4.6.2",
    "eslint-plugin-tailwindcss": "^3.15.1",
    "eslint-plugin-testing-library": "^6.2.2",
    "eslint-plugin-vitest": "^0.5.4",
    "express": "^4.19.2",
    "husky": "^9.0.11",
    "jest-environment-jsdom": "^29.7.0",
    "js-cookie": "^3.0.5",
    "jsdom": "^24.0.0",
    "lint-staged": "^15.2.2",
    "msw": "^2.2.14",
    "pino-http": "^10.1.0",
    "pino-pretty": "^11.1.0",
    "plop": "^4.0.1",
    "pm2": "^5.4.0",
    "postcss": "^8.4.38",
    "prettier": "^3.2.5",
    "storybook": "^8.0.9",
    "tailwindcss": "^3.4.3",
    "tsx": "^4.17.0",
    "typescript": "^5.4.5",
    "vite-tsconfig-paths": "^4.3.2",
    "vitest": "^2.1.4"
  },
  "msw": {
    "workerDirectory": "public"
  }
}


================================================
FILE: apps/nextjs-app/playwright.config.ts
================================================
import { defineConfig, devices } from '@playwright/test';

const PORT = 3000;

/**
 * Read environment variables from file.
 * https://github.com/motdotla/dotenv
 */
// require('dotenv').config();

/**
 * See https://playwright.dev/docs/test-configuration.
 */
export default defineConfig({
  testDir: './e2e',
  /* Run tests in files in parallel */
  fullyParallel: true,
  /* Fail the build on CI if you accidentally left test.only in the source code. */
  forbidOnly: !!process.env.CI,
  /* Retry on CI only */
  retries: process.env.CI ? 2 : 0,
  /* Opt out of parallel tests on CI. */
  workers: process.env.CI ? 1 : undefined,
  /* Reporter to use. See https://playwright.dev/docs/test-reporters */
  reporter: 'html',
  /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
  use: {
    /* Base URL to use in actions like `await page.goto('/')`. */
    // baseURL: 'http://127.0.0.1:3000',

    /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
    trace: 'on-first-retry',
  },

  /* Configure projects for major browsers */
  projects: [
    { name: 'setup', testMatch: /.*\.setup\.ts/ },
    {
      name: 'chromium',
      testMatch: /.*\.spec\.ts/,
      use: {
        ...devices['Desktop Chrome'],
        storageState: 'e2e/.auth/user.json',
      },
      dependencies: ['setup'],
    },
  ],

  /* Run your local dev server before starting the tests */
  webServer: {
    command: `yarn dev --port ${PORT}`,
    timeout: 10 * 1000,
    port: PORT,
    reuseExistingServer: !process.env.CI,
  },
});


================================================
FILE: apps/nextjs-app/plopfile.cjs
================================================
const componentGenerator = require('./generators/component/index');

/**
 *
 * @param {import('plop').NodePlopAPI} plop
 */
module.exports = function (plop) {
  plop.setGenerator('component', componentGenerator);
};


================================================
FILE: apps/nextjs-app/postcss.config.cjs
================================================
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
};


================================================
FILE: apps/nextjs-app/public/_redirects
================================================
/* /index.html 200

================================================
FILE: apps/nextjs-app/public/mockServiceWorker.js
================================================
/* eslint-disable */
/* tslint:disable */

/**
 * Mock Service Worker.
 * @see https://github.com/mswjs/msw
 * - Please do NOT modify this file.
 * - Please do NOT serve this file on production.
 */

const PACKAGE_VERSION = '2.3.5'
const INTEGRITY_CHECKSUM = '26357c79639bfa20d64c0efca2a87423'
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
const activeClientIds = new Set()

self.addEventListener('install', function () {
  self.skipWaiting()
})

self.addEventListener('activate', function (event) {
  event.waitUntil(self.clients.claim())
})

self.addEventListener('message', async function (event) {
  const clientId = event.source.id

  if (!clientId || !self.clients) {
    return
  }

  const client = await self.clients.get(clientId)

  if (!client) {
    return
  }

  const allClients = await self.clients.matchAll({
    type: 'window',
  })

  switch (event.data) {
    case 'KEEPALIVE_REQUEST': {
      sendToClient(client, {
        type: 'KEEPALIVE_RESPONSE',
      })
      break
    }

    case 'INTEGRITY_CHECK_REQUEST': {
      sendToClient(client, {
        type: 'INTEGRITY_CHECK_RESPONSE',
        payload: {
          packageVersion: PACKAGE_VERSION,
          checksum: INTEGRITY_CHECKSUM,
        },
      })
      break
    }

    case 'MOCK_ACTIVATE': {
      activeClientIds.add(clientId)

      sendToClient(client, {
        type: 'MOCKING_ENABLED',
        payload: true,
      })
      break
    }

    case 'MOCK_DEACTIVATE': {
      activeClientIds.delete(clientId)
      break
    }

    case 'CLIENT_CLOSED': {
      activeClientIds.delete(clientId)

      const remainingClients = allClients.filter((client) => {
        return client.id !== clientId
      })

      // Unregister itself when there are no more clients
      if (remainingClients.length === 0) {
        self.registration.unregister()
      }

      break
    }
  }
})

self.addEventListener('fetch', function (event) {
  const { request } = event

  // Bypass navigation requests.
  if (request.mode === 'navigate') {
    return
  }

  // Opening the DevTools triggers the "only-if-cached" request
  // that cannot be handled by the worker. Bypass such requests.
  if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
    return
  }

  // Bypass all requests when there are no active clients.
  // Prevents the self-unregistered worked from handling requests
  // after it's been deleted (still remains active until the next reload).
  if (activeClientIds.size === 0) {
    return
  }

  // Generate unique request ID.
  const requestId = crypto.randomUUID()
  event.respondWith(handleRequest(event, requestId))
})

async function handleRequest(event, requestId) {
  const client = await resolveMainClient(event)
  const response = await getResponse(event, client, requestId)

  // Send back the response clone for the "response:*" life-cycle events.
  // Ensure MSW is active and ready to handle the message, otherwise
  // this message will pend indefinitely.
  if (client && activeClientIds.has(client.id)) {
    ;(async function () {
      const responseClone = response.clone()

      sendToClient(
        client,
        {
          type: 'RESPONSE',
          payload: {
            requestId,
            isMockedResponse: IS_MOCKED_RESPONSE in response,
            type: responseClone.type,
            status: responseClone.status,
            statusText: responseClone.statusText,
            body: responseClone.body,
            headers: Object.fromEntries(responseClone.headers.entries()),
          },
        },
        [responseClone.body],
      )
    })()
  }

  return response
}

// Resolve the main client for the given event.
// Client that issues a request doesn't necessarily equal the client
// that registered the worker. It's with the latter the worker should
// communicate with during the response resolving phase.
async function resolveMainClient(event) {
  const client = await self.clients.get(event.clientId)

  if (client?.frameType === 'top-level') {
    return client
  }

  const allClients = await self.clients.matchAll({
    type: 'window',
  })

  return allClients
    .filter((client) => {
      // Get only those clients that are currently visible.
      return client.visibilityState === 'visible'
    })
    .find((client) => {
      // Find the client ID that's recorded in the
      // set of clients that have registered the worker.
      return activeClientIds.has(client.id)
    })
}

async function getResponse(event, client, requestId) {
  const { request } = event

  // Clone the request because it might've been already used
  // (i.e. its body has been read and sent to the client).
  const requestClone = request.clone()

  function passthrough() {
    const headers = Object.fromEntries(requestClone.headers.entries())

    // Remove internal MSW request header so the passthrough request
    // complies with any potential CORS preflight checks on the server.
    // Some servers forbid unknown request headers.
    delete headers['x-msw-intention']

    return fetch(requestClone, { headers })
  }

  // Bypass mocking when the client is not active.
  if (!client) {
    return passthrough()
  }

  // Bypass initial page load requests (i.e. static assets).
  // The absence of the immediate/parent client in the map of the active clients
  // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
  // and is not ready to handle requests.
  if (!activeClientIds.has(client.id)) {
    return passthrough()
  }

  // Notify the client that a request has been intercepted.
  const requestBuffer = await request.arrayBuffer()
  const clientMessage = await sendToClient(
    client,
    {
      type: 'REQUEST',
      payload: {
        id: requestId,
        url: request.url,
        mode: request.mode,
        method: request.method,
        headers: Object.fromEntries(request.headers.entries()),
        cache: request.cache,
        credentials: request.credentials,
        destination: request.destination,
        integrity: request.integrity,
        redirect: request.redirect,
        referrer: request.referrer,
        referrerPolicy: request.referrerPolicy,
        body: requestBuffer,
        keepalive: request.keepalive,
      },
    },
    [requestBuffer],
  )

  switch (clientMessage.type) {
    case 'MOCK_RESPONSE': {
      return respondWithMock(clientMessage.data)
    }

    case 'PASSTHROUGH': {
      return passthrough()
    }
  }

  return passthrough()
}

function sendToClient(client, message, transferrables = []) {
  return new Promise((resolve, reject) => {
    const channel = new MessageChannel()

    channel.port1.onmessage = (event) => {
      if (event.data && event.data.error) {
        return reject(event.data.error)
      }

      resolve(event.data)
    }

    client.postMessage(
      message,
      [channel.port2].concat(transferrables.filter(Boolean)),
    )
  })
}

async function respondWithMock(response) {
  // Setting response status code to 0 is a no-op.
  // However, when responding with a "Response.error()", the produced Response
  // instance will have status code set to 0. Since it's not possible to create
  // a Response instance with status code 0, handle that use-case separately.
  if (response.status === 0) {
    return Response.error()
  }

  const mockedResponse = new Response(response.body, response)

  Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, {
    value: true,
    enumerable: true,
  })

  return mockedResponse
}


================================================
FILE: apps/nextjs-app/public/robots.txt
================================================
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:


================================================
FILE: apps/nextjs-app/src/app/app/_components/dashboard-info.tsx
================================================
'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>
      )}
    </>
  );
};


================================================
FILE: apps/nextjs-app/src/app/app/_components/dashboard-layout.tsx
================================================
'use client';

import { Home, PanelLeft, Folder, Users, User2 } from 'lucide-react';
import NextLink from 'next/link';
import { useRouter, usePathname } from 'next/navigation';
import { ErrorBoundary } from 'react-error-boundary';

import { Button } from '@/components/ui/button';
import { Drawer, DrawerContent, DrawerTrigger } from '@/components/ui/drawer';
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} 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;
  to: string;
  icon: (props: React.SVGProps<SVGSVGElement>) => JSX.Element;
};

const Logo = () => {
  return (
    <Link className="flex items-center text-white" href={paths.home.getHref()}>
      <img className="h-8 w-auto" src="/logo.svg" alt="Workflow" />
      <span className="text-sm font-semibold text-white">
        Bulletproof React
      </span>
    </Link>
  );
};

const Layout = ({ children }: { children: React.ReactNode }) => {
  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 },
    user.data?.role === 'ADMIN' && {
      name: 'Users',
      to: paths.app.users.getHref(),
      icon: Users,
    },
  ].filter(Boolean) as SideNavigationItem[];

  return (
    <div className="flex min-h-screen w-full flex-col bg-muted/40">
      <aside className="fixed inset-y-0 left-0 z-10 hidden w-60 flex-col border-r bg-black sm:flex">
        <nav className="flex flex-col items-center gap-4 px-2 py-4">
          <div className="flex h-16 shrink-0 items-center px-4">
            <Logo />
          </div>
          {navigation.map((item) => {
            const isActive = pathname === item.to;
            return (
              <NextLink
                key={item.name}
                href={item.to}
                className={cn(
                  'text-gray-300 hover:bg-gray-700 hover:text-white',
                  'group flex flex-1 w-full items-center rounded-md p-2 text-base font-medium',
                  isActive && 'bg-gray-900 text-white',
                )}
              >
                <item.icon
                  className={cn(
                    'text-gray-400 group-hover:text-gray-300',
                    'mr-4 size-6 shrink-0',
                  )}
                  aria-hidden="true"
                />
                {item.name}
              </NextLink>
            );
          })}
        </nav>
      </aside>
      <div className="flex flex-col sm:gap-4 sm:py-4 sm:pl-60">
        <header className="sticky top-0 z-30 flex h-14 items-center justify-between gap-4 border-b bg-background px-4 sm:static sm:h-auto sm:justify-end sm:border-0 sm:bg-transparent sm:px-6">
          {/* <Progress /> */}
          <Drawer>
            <DrawerTrigger asChild>
              <Button size="icon" variant="outline" className="sm:hidden">
                <PanelLeft className="size-5" />
                <span className="sr-only">Toggle Menu</span>
              </Button>
            </DrawerTrigger>
            <DrawerContent
              side="left"
              className="bg-black pt-10 text-white sm:max-w-60"
            >
              <nav className="grid gap-6 text-lg font-medium">
                <div className="flex h-16 shrink-0 items-center px-4">
                  <Logo />
                </div>
                {navigation.map((item) => {
                  const isActive = pathname === item.to;
                  return (
                    <NextLink
                      key={item.name}
                      href={item.to}
                      className={cn(
                        'text-gray-300 hover:bg-gray-700 hover:text-white',
                        'group flex flex-1 w-full items-center rounded-md p-2 text-base font-medium',
                        isActive && 'bg-gray-900 text-white',
                      )}
                    >
                      <item.icon
                        className={cn(
                          'text-gray-400 group-hover:text-gray-300',
                          'mr-4 size-6 shrink-0',
                        )}
                        aria-hidden="true"
                      />
                      {item.name}
                    </NextLink>
                  );
                })}
              </nav>
            </DrawerContent>
          </Drawer>
          <DropdownMenu>
            <DropdownMenuTrigger asChild>
              <Button
                variant="outline"
                size="icon"
                className="overflow-hidden rounded-full"
              >
                <span className="sr-only">Open user menu</span>
                <User2 className="size-6 rounded-full" />
              </Button>
            </DropdownMenuTrigger>
            <DropdownMenuContent align="end">
              <DropdownMenuItem
                onClick={() => router.push(paths.app.profile.getHref())}
                className={cn('block px-4 py-2 text-sm text-gray-700')}
              >
                Your Profile
              </DropdownMenuItem>
              <DropdownMenuSeparator />
              <DropdownMenuItem
                className={cn('block px-4 py-2 text-sm text-gray-700 w-full')}
                onClick={() => logout.mutate()}
              >
                Sign Out
              </DropdownMenuItem>
            </DropdownMenuContent>
          </DropdownMenu>
        </header>
        <main className="grid flex-1 items-start gap-4 p-4 sm:px-6 sm:py-0 md:gap-8">
          {children}
        </main>
      </div>
    </div>
  );
};

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

export const DashboardLayout = ({
  children,
}: {
  children: React.ReactNode;
}) => {
  const pathname = usePathname();
  return (
    <Layout>
      <ErrorBoundary key={pathname} FallbackComponent={Fallback}>
        {children}
      </ErrorBoundary>
    </Layout>
  );
};


================================================
FILE: apps/nextjs-app/src/app/app/discussions/[discussionId]/__tests__/discussion.test.tsx
================================================
import { useParams } from 'next/navigation';

import {
  renderApp,
  screen,
  userEvent,
  waitFor,
  createDiscussion,
  createUser,
  within,
  waitForLoadingToFinish,
} from '@/testing/test-utils';

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

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 });

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

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

  await waitForLoadingToFinish();

  await screen.findByText(fakeDiscussion.title);

  return {
    ...utils,
    fakeUser,
    fakeDiscussion,
  };
};

test('should render discussion', async () => {
  const { fakeDiscussion } = await renderDiscussion();
  expect(screen.getByText(fakeDiscussion.body)).toBeInTheDocument();
});

test('should update discussion', async () => {
  const { fakeDiscussion } = await renderDiscussion();

  const titleUpdate = '-Updated';
  const bodyUpdate = '-Updated';

  await userEvent.click(
    screen.getByRole('button', { name: /update discussion/i }),
  );

  const drawer = await screen.findByRole('dialog', {
    name: /update discussion/i,
  });

  const titleField = within(drawer).getByText(/title/i);
  const bodyField = within(drawer).getByText(/body/i);

  const newTitle = `${fakeDiscussion.title}${titleUpdate}`;
  const newBody = `${fakeDiscussion.body}${bodyUpdate}`;

  // replacing the title with the new title
  await userEvent.type(titleField, newTitle);

  // appending updated to the body
  await userEvent.type(bodyField, bodyUpdate);

  const submitButton = within(drawer).getByRole('button', {
    name: /submit/i,
  });

  await userEvent.click(submitButton);

  await waitFor(() => expect(drawer).not.toBeInTheDocument());

  expect(
    await screen.findByRole('heading', { name: newTitle }),
  ).toBeInTheDocument();
  expect(await screen.findByText(newBody)).toBeInTheDocument();
});

test(
  'should create and delete a comment on the discussion',
  async () => {
    await renderDiscussion();

    const comment = 'Hello World';

    await userEvent.click(
      screen.getByRole('button', { name: /create comment/i }),
    );

    const drawer = await screen.findByRole('dialog', {
      name: /create comment/i,
    });

    const bodyField = await within(drawer).findByText(/body/i);

    await userEvent.type(bodyField, comment);

    const submitButton = await within(drawer).findByRole('button', {
      name: /submit/i,
    });

    await userEvent.click(submitButton);

    await waitFor(() => expect(drawer).not.toBeInTheDocument());

    await screen.findByText(comment);

    const commentsList = await screen.findByRole('list', {
      name: 'comments',
    });

    const commentElements =
      await within(commentsList).findAllByRole('listitem');

    const commentElement = commentElements[0];

    expect(commentElement).toBeInTheDocument();

    const deleteCommentButton = within(commentElement).getByRole('button', {
      name: /delete comment/i,
      // exact: false,
    });

    await userEvent.click(deleteCommentButton);

    const confirmationDialog = await screen.findByRole('dialog', {
      name: /delete comment/i,
    });

    const confirmationDeleteButton = await within(
      confirmationDialog,
    ).findByRole('button', {
      name: /delete/i,
    });

    await userEvent.click(confirmationDeleteButton);

    await screen.findByText(/comment deleted/i);

    await waitFor(() => {
      expect(within(commentsList).queryByText(comment)).not.toBeInTheDocument();
    });
  },
  {
    timeout: 20000,
  },
);


================================================
FILE: apps/nextjs-app/src/app/app/discussions/[discussionId]/_components/discussion.tsx
================================================
'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>
  );
};


================================================
FILE: apps/nextjs-app/src/app/app/discussions/[discussionId]/page.tsx
================================================
import {
  dehydrate,
  HydrationBoundary,
  QueryClient,
} from '@tanstack/react-query';

import { getInfiniteCommentsQueryOptions } from '@/features/comments/api/get-comments';
import {
  getDiscussion,
  getDiscussionQueryOptions,
} from '@/features/discussions/api/get-discussion';

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

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

  const discussion = await getDiscussion({ discussionId });

  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 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?.data) return <div>Discussion not found</div>;

  return (
    <HydrationBoundary state={dehydratedState}>
      <Discussion discussionId={discussionId} />
    </HydrationBoundary>
  );
};

export default DiscussionPage;


================================================
FILE: apps/nextjs-app/src/app/app/discussions/__tests__/discussions.test.tsx
================================================
import type { Mock } from 'vitest';

import { createDiscussion } from '@/testing/data-generators';
import {
  renderApp,
  screen,
  userEvent,
  waitFor,
  waitForLoadingToFinish,
  within,
} from '@/testing/test-utils';
import { formatDate } from '@/utils/format';

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

beforeAll(() => {
  vi.spyOn(console, 'error').mockImplementation(() => {});
});

afterAll(() => {
  (console.error as Mock).mockRestore();
});

test(
  'should create, render and delete discussions',
  { timeout: 10000 },
  async () => {
    await renderApp(<Discussions />);

    await waitForLoadingToFinish();

    const newDiscussion = createDiscussion();

    expect(await screen.findByText(/no entries/i)).toBeInTheDocument();

    await userEvent.click(
      screen.getByRole('button', { name: /create discussion/i }),
    );

    const drawer = await screen.findByRole('dialog', {
      name: /create discussion/i,
    });

    const titleField = within(drawer).getByText(/title/i);
    const bodyField = within(drawer).getByText(/body/i);

    await userEvent.type(titleField, newDiscussion.title);
    await userEvent.type(bodyField, newDiscussion.body);

    const submitButton = within(drawer).getByRole('button', {
      name: /submit/i,
    });

    await userEvent.click(submitButton);

    await waitFor(() => expect(drawer).not.toBeInTheDocument());

    const row = await screen.findByRole(
      'row',
      {
        name: `${newDiscussion.title} ${formatDate(newDiscussion.createdAt)} View Delete Discussion`,
      },
      { timeout: 5000 },
    );

    expect(
      within(row).getByRole('cell', {
        name: newDiscussion.title,
      }),
    ).toBeInTheDocument();

    await userEvent.click(
      within(row).getByRole('button', {
        name: /delete discussion/i,
      }),
    );

    const confirmationDialog = await screen.findByRole('dialog', {
      name: /delete discussion/i,
    });

    const confirmationDeleteButton = within(confirmationDialog).getByRole(
      'button',
      {
        name: /delete discussion/i,
      },
    );

    await userEvent.click(confirmationDeleteButton);

    await screen.findByText(/discussion deleted/i);

    expect(
      within(row).queryByRole('cell', {
        name: newDiscussion.title,
      }),
    ).not.toBeInTheDocument();
  },
);


================================================
FILE: apps/nextjs-app/src/app/app/discussions/_components/discussions.tsx
================================================
'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>
  );
};


================================================
FILE: apps/nextjs-app/src/app/app/discussions/page.tsx
================================================
import {
  dehydrate,
  HydrationBoundary,
  QueryClient,
} from '@tanstack/react-query';

import { getDiscussionsQueryOptions } from '@/features/discussions/api/get-discussions';

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

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 (
    <HydrationBoundary state={dehydratedState}>
      <Discussions />
    </HydrationBoundary>
  );
};

export default DiscussionsPage;


================================================
FILE: apps/nextjs-app/src/app/app/layout.tsx
================================================
import { ReactNode } from 'react';

import { DashboardLayout } from './_components/dashboard-layout';

export const metadata = {
  title: 'Dashboard',
  description: 'Dashboard',
};

const AppLayout = ({ children }: { children: ReactNode }) => {
  return <DashboardLayout>{children}</DashboardLayout>;
};

export default AppLayout;


================================================
FILE: apps/nextjs-app/src/app/app/page.tsx
================================================
import { DashboardInfo } from './_components/dashboard-info';

export const metadata = {
  title: 'Dashboard',
  description: 'Dashboard',
};

const DashboardPage = async () => {
  return <DashboardInfo />;
};

export default DashboardPage;


================================================
FILE: apps/nextjs-app/src/app/app/profile/_components/profile.tsx
================================================
'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) => (
  <div className="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
    <dt className="text-sm font-medium text-gray-500">{label}</dt>
    <dd className="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
      {value}
    </dd>
  </div>
);

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

  if (!user) return null;

  return (
    <div className="overflow-hidden bg-white shadow sm:rounded-lg">
      <div className="px-4 py-5 sm:px-6">
        <div className="flex justify-between">
          <h3 className="text-lg font-medium leading-6 text-gray-900">
            User Information
          </h3>
          <UpdateProfile />
        </div>
        <p className="mt-1 max-w-2xl text-sm text-gray-500">
          Personal details of the user.
        </p>
      </div>
      <div className="border-t border-gray-200 px-4 py-5 sm:p-0">
        <dl className="sm:divide-y sm:divide-gray-200">
          <Entry label="First Name" value={user.data?.firstName ?? ''} />
          <Entry label="Last Name" value={user.data?.lastName ?? ''} />
          <Entry label="Email Address" value={user.data?.email ?? ''} />
          <Entry label="Role" value={user.data?.role ?? ''} />
          <Entry label="Bio" value={user.data?.bio ?? ''} />
        </dl>
      </div>
    </div>
  );
};


================================================
FILE: apps/nextjs-app/src/app/app/profile/page.tsx
================================================
import { Profile } from './_components/profile';

export const metadata = {
  title: 'Profile',
  description: 'Profile',
};

const ProfilePage = () => {
  return <Profile />;
};

export default ProfilePage;


================================================
FILE: apps/nextjs-app/src/app/app/users/_components/admin-guard.tsx
================================================
'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 <Spinner className="m-4" />;
  }

  if (!canViewUsers(user?.data)) {
    return <div>Only admin can view this.</div>;
  }

  return children;
};


================================================
FILE: apps/nextjs-app/src/app/app/users/_components/users.tsx
================================================
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 (
    <HydrationBoundary state={dehydratedState}>
      <UsersList />
    </HydrationBoundary>
  );
};


================================================
FILE: apps/nextjs-app/src/app/app/users/page.tsx
================================================
import { ContentLayout } from '@/components/layouts/content-layout';

import { AdminGuard } from './_components/admin-guard';
import { Users } from './_components/users';

export const metadata = {
  title: 'Users',
  description: 'Users',
};

const UsersPage = () => {
  return (
    <ContentLayout title="Users">
      <AdminGuard>
        <Users />
      </AdminGuard>
    </ContentLayout>
  );
};

export default UsersPage;


================================================
FILE: apps/nextjs-app/src/app/auth/_components/auth-layout.tsx
================================================
'use client';

import { useRouter, usePathname, useSearchParams } from 'next/navigation';
import { ReactNode, useEffect } from 'react';

import { Link } from '@/components/ui/link';
import { paths } from '@/config/paths';
import { useUser } from '@/lib/auth';

type LayoutProps = {
  children: ReactNode;
};

export const AuthLayout = ({ children }: LayoutProps) => {
  const user = useUser();
  const router = useRouter();
  const pathname = usePathname();
  const isLoginPage = pathname === paths.auth.login.getHref();
  const title = isLoginPage
    ? 'Log in to your account'
    : 'Register your account';

  const searchParams = useSearchParams();
  const redirectTo = searchParams?.get('redirectTo');

  useEffect(() => {
    if (user.data) {
      router.replace(
        `${redirectTo ? `${decodeURIComponent(redirectTo)}` : paths.app.dashboard.getHref()}`,
      );
    }
  }, [user.data, router, redirectTo]);

  return (
    <div className="flex min-h-screen flex-col justify-center bg-gray-50 py-12 sm:px-6 lg:px-8">
      <div className="sm:mx-auto sm:w-full sm:max-w-md">
        <div className="flex justify-center">
          <Link
            className="flex items-center text-white"
            href={paths.home.getHref()}
          >
            <img className="h-24 w-auto" src="/logo.svg" alt="Workflow" />
          </Link>
        </div>

        <h2 className="mt-3 text-center text-3xl font-extrabold text-gray-900">
          {title}
        </h2>
      </div>

      <div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
        <div className="bg-white px-4 py-8 shadow sm:rounded-lg sm:px-10">
          {children}
        </div>
      </div>
    </div>
  );
};


================================================
FILE: apps/nextjs-app/src/app/auth/layout.tsx
================================================
import { ReactNode, Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';

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',
};

const AuthLayout = ({ children }: { children: ReactNode }) => {
  return (
    <Suspense
      fallback={
        <div className="flex size-full items-center justify-center">
          <Spinner size="xl" />
        </div>
      }
    >
      <ErrorBoundary fallback={<div>Something went wrong!</div>}>
        <AuthLayoutComponent>{children}</AuthLayoutComponent>
      </ErrorBoundary>
    </Suspense>
  );
};

export default AuthLayout;


================================================
FILE: apps/nextjs-app/src/app/auth/login/page.tsx
================================================
'use client';

import { useRouter, useSearchParams } from 'next/navigation';

import { paths } from '@/config/paths';
import { LoginForm } from '@/features/auth/components/login-form';

const LoginPage = () => {
  const router = useRouter();
  const searchParams = useSearchParams();
  const redirectTo = searchParams?.get('redirectTo');

  return (
    <LoginForm
      onSuccess={() =>
        router.replace(
          `${redirectTo ? `${decodeURIComponent(redirectTo)}` : paths.app.dashboard.getHref()}`,
        )
      }
    />
  );
};

export default LoginPage;


================================================
FILE: apps/nextjs-app/src/app/auth/register/page.tsx
================================================
'use client';

import { useRouter, useSearchParams } from 'next/navigation';
import { useState } from 'react';

import { paths } from '@/config/paths';
import { RegisterForm } from '@/features/auth/components/register-form';
import { useTeams } from '@/features/teams/api/get-teams';

const RegisterPage = () => {
  const router = useRouter();

  const searchParams = useSearchParams();
  const redirectTo = searchParams?.get('redirectTo');

  const [chooseTeam, setChooseTeam] = useState(false);

  const teamsQuery = useTeams({
    queryConfig: {
      enabled: chooseTeam,
    },
  });

  return (
    <RegisterForm
      onSuccess={() =>
        router.replace(
          `${redirectTo ? `${decodeURIComponent(redirectTo)}` : paths.app.dashboard.getHref()}`,
        )
      }
      chooseTeam={chooseTeam}
      setChooseTeam={() => setChooseTeam(!chooseTeam)}
      teams={teamsQuery.data?.data}
    />
  );
};

export default RegisterPage;


================================================
FILE: apps/nextjs-app/src/app/layout.tsx
================================================
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';

export const metadata = {
  title: 'Bulletproof React',
  description: 'Showcasing Best Practices For Building React Applications',
};

const RootLayout = async ({ children }: { children: ReactNode }) => {
  const queryClient = new QueryClient();

  await queryClient.prefetchQuery(getUserQueryOptions());

  const dehydratedState = dehydrate(queryClient);

  return (
    <html lang="en">
      <body>
        <AppProvider>
          <HydrationBoundary state={dehydratedState}>
            {children}
          </HydrationBoundary>
        </AppProvider>
      </body>
    </html>
  );
};

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';


================================================
FILE: apps/nextjs-app/src/app/not-found.tsx
================================================
import { Link } from '@/components/ui/link';
import { paths } from '@/config/paths';

const NotFoundPage = () => {
  return (
    <div className="mt-52 flex flex-col items-center font-semibold">
      <h1>404 - Not Found</h1>
      <p>Sorry, the page you are looking for does not exist.</p>
      <Link href={paths.home.getHref()} replace>
        Go to Home
      </Link>
    </div>
  );
};

export default NotFoundPage;


================================================
FILE: apps/nextjs-app/src/app/page.tsx
================================================
import { Button } from '@/components/ui/button';
import { Link } from '@/components/ui/link';
import { paths } from '@/config/paths';
import { checkLoggedIn } from '@/utils/auth';

const HomePage = () => {
  const isLoggedIn = checkLoggedIn();

  return (
    <div className="flex h-screen items-center bg-white">
      <div className="mx-auto max-w-7xl px-4 py-12 text-center sm:px-6 lg:px-8 lg:py-16">
        <h2 className="text-3xl font-extrabold tracking-tight text-gray-900 sm:text-4xl">
          <span className="block">Bulletproof React</span>
        </h2>
        <img src="/logo.svg" alt="react" />
        <p>Showcasing Best Practices For Building React Applications</p>
        <div className="mt-8 flex justify-center">
          <div className="inline-flex rounded-md shadow">
            <Link
              href={
                isLoggedIn
                  ? paths.app.root.getHref()
                  : paths.auth.login.getHref()
              }
            >
              <Button
                icon={
                  <svg
                    xmlns="http://www.w3.org/2000/svg"
                    className="size-6"
                    fill="none"
                    viewBox="0 0 24 24"
                    stroke="currentColor"
                  >
                    <path
                      strokeLinecap="round"
                      strokeLinejoin="round"
                      strokeWidth="2"
                      d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
                    />
                  </svg>
                }
              >
                Get started
              </Button>
            </Link>
          </div>
          <div className="ml-3 inline-flex">
            <a
              href="https://github.com/alan2207/bulletproof-react"
              target="_blank"
              rel="noreferrer"
            >
              <Button
                variant="outline"
                icon={
                  <svg
                    fill="currentColor"
                    viewBox="0 0 24 24"
                    className="size-6"
                  >
                    <path
                      fillRule="evenodd"
                      d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
                      clipRule="evenodd"
                    />
                  </svg>
                }
              >
                Github Repo
              </Button>
            </a>
          </div>
        </div>
      </div>
    </div>
  );
};

export default HomePage;


================================================
FILE: apps/nextjs-app/src/app/provider.tsx
================================================
'use client';

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import * as React from 'react';
import { ErrorBoundary } from 'react-error-boundary';

import { MainErrorFallback } from '@/components/errors/main';
import { Notifications } from '@/components/ui/notifications';
import { queryConfig } from '@/lib/react-query';

type AppProviderProps = {
  children: React.ReactNode;
};

export const AppProvider = ({ children }: AppProviderProps) => {
  const [queryClient] = React.useState(
    () =>
      new QueryClient({
        defaultOptions: queryConfig,
      }),
  );

  return (
    <ErrorBoundary FallbackComponent={MainErrorFallback}>
      <QueryClientProvider client={queryClient}>
        {process.env.DEV && <ReactQueryDevtools />}
        <Notifications />
        {children}
      </QueryClientProvider>
    </ErrorBoundary>
  );
};


================================================
FILE: apps/nextjs-app/src/app/public/discussions/[discussionId]/page.tsx
================================================
import {
  dehydrate,
  HydrationBoundary,
  QueryClient,
} from '@tanstack/react-query';

import { Discussion } from '@/app/app/discussions/[discussionId]/_components/discussion';
import { getInfiniteCommentsQueryOptions } from '@/features/comments/api/get-comments';
import {
  getDiscussion,
  getDiscussionQueryOptions,
} from '@/features/discussions/api/get-discussion';

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

  const discussion = await getDiscussion({ discussionId });

  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),
    ),
  ]);

  return {
    dehydratedState: dehydrate(queryClient),
  };
};

const PublicDiscussionPage = async ({
  params: { discussionId },
}: {
  params: {
    discussionId: string;
  };
}) => {
  const { dehydratedState } = await preloadData(discussionId);
  return (
    <HydrationBoundary state={dehydratedState}>
      <Discussion discussionId={discussionId} />
    </HydrationBoundary>
  );
};

export default PublicDiscussionPage;


================================================
FILE: apps/nextjs-app/src/components/errors/main.tsx
================================================
import { Button } from '../ui/button';

export const MainErrorFallback = () => {
  return (
    <div
      className="flex h-screen w-screen flex-col items-center justify-center text-red-500"
      role="alert"
    >
      <h2 className="text-lg font-semibold">Ooops, something went wrong :( </h2>
      <Button
        className="mt-4"
        onClick={() => window.location.assign(window.location.origin)}
      >
        Refresh
      </Button>
    </div>
  );
};


================================================
FILE: apps/nextjs-app/src/components/layouts/content-layout.tsx
================================================
import { ReactNode } from 'react';

type ContentLayoutProps = {
  children: ReactNode;
  title?: string;
};

export const ContentLayout = ({ children, title = '' }: ContentLayoutProps) => {
  return (
    <div className="py-6">
      <div className="mx-auto max-w-7xl px-4 sm:px-6 md:px-8">
        <h1 className="text-2xl font-semibold text-gray-900">{title}</h1>
      </div>
      <div className="mx-auto max-w-7xl px-4 py-6 sm:px-6 md:px-8">
        {children}
      </div>
    </div>
  );
};


================================================
FILE: apps/nextjs-app/src/components/ui/button/button.stories.tsx
================================================
import { Meta, StoryObj } from '@storybook/react';

import { Button } from './button';

const meta: Meta<typeof Button> = {
  component: Button,
};

export default meta;
type Story = StoryObj<typeof Button>;

export const Default: Story = {
  args: {
    children: 'Button',
    variant: 'default',
  },
};


================================================
FILE: apps/nextjs-app/src/components/ui/button/button.tsx
================================================
import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority';
import * as React from 'react';

import { cn } from '@/utils/cn';

import { Spinner } from '../spinner';

const buttonVariants = cva(
  'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
  {
    variants: {
      variant: {
        default:
          'bg-primary text-primary-foreground shadow hover:bg-primary/90',
        destructive:
          'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
        outline:
          'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
        secondary:
          'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
        ghost: 'hover:bg-accent hover:text-accent-foreground',
        link: 'text-primary underline-offset-4 hover:underline',
      },
      size: {
        default: 'h-9 px-4 py-2',
        sm: 'h-8 rounded-md px-3 text-xs',
        lg: 'h-10 rounded-md px-8',
        icon: 'size-9',
      },
    },
    defaultVariants: {
      variant: 'default',
      size: 'default',
    },
  },
);

export type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> &
  VariantProps<typeof buttonVariants> & {
    asChild?: boolean;
    isLoading?: boolean;
    icon?: React.ReactNode;
  };

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  (
    {
      className,
      variant,
      size,
      asChild = false,
      children,
      isLoading,
      icon,
      ...props
    },
    ref,
  ) => {
    const Comp = asChild ? Slot : 'button';
    return (
      <Comp
        className={cn(buttonVariants({ variant, size, className }))}
        ref={ref}
        {...props}
      >
        {isLoading && <Spinner size="sm" className="text-current" />}
        {!isLoading && icon && <span className="mr-2">{icon}</span>}
        <span className="mx-2">{children}</span>
      </Comp>
    );
  },
);
Button.displayName = 'Button';

export { Button, buttonVariants };


================================================
FILE: apps/nextjs-app/src/components/ui/button/index.ts
================================================
export * from './button';


================================================
FILE: apps/nextjs-app/src/components/ui/dialog/__tests__/dialog.test.tsx
================================================
import * as React from 'react';

import { Button } from '@/components/ui/button';
import { useDisclosure } from '@/hooks/use-disclosure';
import { rtlRender, screen, userEvent, waitFor } from '@/testing/test-utils';

import {
  Dialog,
  DialogContent,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from '../dialog';

const openButtonText = 'Open Modal';
const cancelButtonText = 'Cancel';
const titleText = 'Modal Title';

const TestDialog = () => {
  const { close, open, isOpen } = useDisclosure();
  const cancelButtonRef = React.useRef(null);

  return (
    <Dialog
      open={isOpen}
      onOpenChange={(isOpen) => {
        if (!isOpen) {
          close();
        } else {
          open();
        }
      }}
    >
      <DialogTrigger asChild>
        <Button variant="outline">{openButtonText}</Button>
      </DialogTrigger>
      <DialogContent className="sm:max-w-[425px]">
        <DialogHeader>
          <DialogTitle>{titleText}</DialogTitle>
        </DialogHeader>

        <DialogFooter>
          <Button type="submit">Submit</Button>
          <Button ref={cancelButtonRef} variant="outline" onClick={close}>
            {cancelButtonText}
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
};

test('should handle basic dialog flow', async () => {
  rtlRender(<TestDialog />);

  expect(screen.queryByText(titleText)).not.toBeInTheDocument();

  await userEvent.click(screen.getByRole('button', { name: openButtonText }));

  expect(await screen.findByText(titleText)).toBeInTheDocument();

  await userEvent.click(screen.getByRole('button', { name: cancelButtonText }));

  await waitFor(() =>
    expect(screen.queryByText(titleText)).not.toBeInTheDocument(),
  );
});


================================================
FILE: apps/nextjs-app/src/components/ui/dialog/confirmation-dialog/__tests__/confirmation-dialog.test.tsx
================================================
import { Button } from '@/components/ui/button';
import { rtlRender, screen, userEvent, waitFor } from '@/testing/test-utils';

import { ConfirmationDialog } from '../confirmation-dialog';

test('should handle confirmation flow', async () => {
  const titleText = 'Are you sure?';
  const bodyText = 'Are you sure you want to delete this item?';
  const confirmationButtonText = 'Confirm';
  const openButtonText = 'Open';

  await rtlRender(
    <ConfirmationDialog
      icon="danger"
      title={titleText}
      body={bodyText}
      confirmButton={<Button>{confirmationButtonText}</Button>}
      triggerButton={<Button>{openButtonText}</Button>}
    />,
  );

  expect(screen.queryByText(titleText)).not.toBeInTheDocument();

  await userEvent.click(screen.getByRole('button', { name: openButtonText }));

  expect(await screen.findByText(titleText)).toBeInTheDocument();

  expect(screen.getByText(bodyText)).toBeInTheDocument();

  await userEvent.click(screen.getByRole('button', { name: 'Cancel' }));

  await waitFor(() =>
    expect(screen.queryByText(titleText)).not.toBeInTheDocument(),
  );

  expect(screen.queryByText(bodyText)).not.toBeInTheDocument();
});


================================================
FILE: apps/nextjs-app/src/components/ui/dialog/confirmation-dialog/confirmation-dialog.stories.tsx
================================================
import { Meta, StoryObj } from '@storybook/react';

import { Button } from '@/components/ui/button';

import { ConfirmationDialog } from './confirmation-dialog';

const meta: Meta<typeof ConfirmationDialog> = {
  component: ConfirmationDialog,
};

export default meta;

type Story = StoryObj<typeof ConfirmationDialog>;

export const Danger: Story = {
  args: {
    icon: 'danger',
    title: 'Confirmation',
    body: 'Hello World',
    confirmButton: <Button className="bg-red-500">Confirm</Button>,
    triggerButton: <Button>Open</Button>,
  },
};

export const Info: Story = {
  args: {
    icon: 'info',
    title: 'Confirmation',
    body: 'Hello World',
    confirmButton: <Button>Confirm</Button>,
    triggerButton: <Button>Open</Button>,
  },
};


================================================
FILE: apps/nextjs-app/src/components/ui/dialog/confirmation-dialog/confirmation-dialog.tsx
================================================
'use client';

import { CircleAlert, Info } from 'lucide-react';
import * as React from 'react';
import { useEffect } from 'react';

import { Button } from '@/components/ui/button';
import { useDisclosure } from '@/hooks/use-disclosure';

import {
  Dialog,
  DialogContent,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from '../dialog';

export type ConfirmationDialogProps = {
  triggerButton: React.ReactElement;
  confirmButton: React.ReactElement;
  title: string;
  body?: string;
  cancelButtonText?: string;
  icon?: 'danger' | 'info';
  isDone?: boolean;
};

export const ConfirmationDialog = ({
  triggerButton,
  confirmButton,
  title,
  body = '',
  cancelButtonText = 'Cancel',
  icon = 'danger',
  isDone = false,
}: ConfirmationDialogProps) => {
  const { close, open, isOpen } = useDisclosure();
  const cancelButtonRef = React.useRef(null);

  useEffect(() => {
    if (isDone) {
      close();
    }
  }, [isDone, close]);

  return (
    <Dialog
      open={isOpen}
      onOpenChange={(isOpen) => {
        if (!isOpen) {
          close();
        } else {
          open();
        }
      }}
    >
      <DialogTrigger asChild>{triggerButton}</DialogTrigger>
      <DialogContent className="sm:max-w-[425px]">
        <DialogHeader className="flex">
          <DialogTitle className="flex items-center gap-2">
            {' '}
            {icon === 'danger' && (
              <CircleAlert className="size-6 text-red-600" aria-hidden="true" />
            )}
            {icon === 'info' && (
              <Info className="size-6 text-blue-600" aria-hidden="true" />
            )}
            {title}
          </DialogTitle>
        </DialogHeader>

        <div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
          {body && (
            <div className="mt-2">
              <p>{body}</p>
            </div>
          )}
        </div>

        <DialogFooter>
          {confirmButton}
          <Button ref={cancelButtonRef} variant="outline" onClick={close}>
            {cancelButtonText}
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
};


================================================
FILE: apps/nextjs-app/src/components/ui/dialog/confirmation-dialog/index.ts
================================================
export * from './confirmation-dialog';


================================================
FILE: apps/nextjs-app/src/components/ui/dialog/dialog.stories.tsx
================================================
import { Meta, StoryObj } from '@storybook/react';
import * as React from 'react';

import { Button } from '@/components/ui/button';
import { useDisclosure } from '@/hooks/use-disclosure';

import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from './dialog';

const DemoDialog = () => {
  const { close, open, isOpen } = useDisclosure();
  const cancelButtonRef = React.useRef(null);

  return (
    <Dialog
      open={isOpen}
      onOpenChange={(isOpen) => {
        if (!isOpen) {
          close();
        } else {
          open();
        }
      }}
    >
      <DialogTrigger asChild>
        <Button variant="outline">Open Dialog</Button>
      </DialogTrigger>
      <DialogContent className="sm:max-w-[425px]">
        <DialogHeader>
          <DialogTitle>Edit profile</DialogTitle>
          <DialogDescription>Lorem ipsum</DialogDescription>
        </DialogHeader>
        <div className="grid gap-4 py-4">Lorem ipsum</div>

        <DialogFooter>
          <Button type="submit">Save changes</Button>
          <Button ref={cancelButtonRef} variant="outline" onClick={close}>
            Cancel
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
};

const meta: Meta = {
  component: Dialog,
};

export default meta;

type Story = StoryObj<typeof Dialog>;

export const Demo: Story = {
  render: () => <DemoDialog />,
};


================================================
FILE: apps/nextjs-app/src/components/ui/dialog/dialog.tsx
================================================
'use client';

import * as DialogPrimitive from '@radix-ui/react-dialog';
import { Cross2Icon } from '@radix-ui/react-icons';
import * as React from 'react';

import { cn } from '@/utils/cn';

const Dialog = DialogPrimitive.Root;

const DialogTrigger = DialogPrimitive.Trigger;

const DialogPortal = DialogPrimitive.Portal;

const DialogClose = DialogPrimitive.Close;

const DialogOverlay = React.forwardRef<
  React.ElementRef<typeof DialogPrimitive.Overlay>,
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
  <DialogPrimitive.Overlay
    ref={ref}
    className={cn(
      'fixed inset-0 z-50 bg-black/80  data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
      className,
    )}
    {...props}
  />
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;

const DialogContent = React.forwardRef<
  React.ElementRef<typeof DialogPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
  <DialogPortal>
    <DialogOverlay />
    <DialogPrimitive.Content
      ref={ref}
      className={cn(
        'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
        className,
      )}
      {...props}
    >
      {children}
      <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
        <Cross2Icon className="size-4" />
        <span className="sr-only">Close</span>
      </DialogPrimitive.Close>
    </DialogPrimitive.Content>
  </DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;

const DialogHeader = ({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) => (
  <div
    className={cn(
      'flex flex-col space-y-1.5 text-center sm:text-left',
      className,
    )}
    {...props}
  />
);
DialogHeader.displayName = 'DialogHeader';

const DialogFooter = ({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) => (
  <div
    className={cn(
      'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
      className,
    )}
    {...props}
  />
);
DialogFooter.displayName = 'DialogFooter';

const DialogTitle = React.forwardRef<
  React.ElementRef<typeof DialogPrimitive.Title>,
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
  <DialogPrimitive.Title
    ref={ref}
    className={cn(
      'text-lg font-semibold leading-none tracking-tight',
      className,
    )}
    {...props}
  />
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;

const DialogDescription = React.forwardRef<
  React.ElementRef<typeof DialogPrimitive.Description>,
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
  <DialogPrimitive.Description
    ref={ref}
    className={cn('text-sm text-muted-foreground', className)}
    {...props}
  />
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;

export {
  Dialog,
  DialogPortal,
  DialogOverlay,
  DialogTrigger,
  DialogClose,
  DialogContent,
  DialogHeader,
  DialogFooter,
  DialogTitle,
  DialogDescription,
};


================================================
FILE: apps/nextjs-app/src/components/ui/dialog/index.ts
================================================
export * from './dialog';
export * from './confirmation-dialog';


================================================
FILE: apps/nextjs-app/src/components/ui/drawer/__tests__/drawer.test.tsx
================================================
import { Button } from '@/components/ui/button';
import { rtlRender, screen, userEvent, waitFor } from '@/testing/test-utils';

import {
  Drawer,
  DrawerClose,
  DrawerContent,
  DrawerFooter,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
} from '../drawer';

const openButtonText = 'Open Drawer';
const titleText = 'Drawer Title';
const cancelButtonText = 'Cancel';
const drawerContentText = 'Hello From Drawer';

const TestDrawer = () => {
  return (
    <Drawer>
      <DrawerTrigger asChild>
        <Button variant="outline">{openButtonText}</Button>
      </DrawerTrigger>
      <DrawerContent className="flex max-w-[800px] flex-col justify-between sm:max-w-[540px]">
        <div className="flex flex-col">
          <DrawerHeader>
            <DrawerTitle>{titleText}</DrawerTitle>
          </DrawerHeader>
          <div>{drawerContentText}</div>
        </div>
        <DrawerFooter>
          <DrawerClose asChild>
            <Button value="outline" type="submit">
              {cancelButtonText}
            </Button>
          </DrawerClose>
        </DrawerFooter>
      </DrawerContent>
    </Drawer>
  );
};

test('should handle basic drawer flow', async () => {
  await rtlRender(<TestDrawer />);

  expect(screen.queryByText(titleText)).not.toBeInTheDocument();

  await userEvent.click(
    screen.getByRole('button', {
      name: openButtonText,
    }),
  );

  expect(await screen.findByText(titleText)).toBeInTheDocument();

  await userEvent.click(
    screen.getByRole('button', {
      name: cancelButtonText,
    }),
  );

  await waitFor(() =>
    expect(screen.queryByText(titleText)).not.toBeInTheDocument(),
  );
});


================================================
FILE: apps/nextjs-app/src/components/ui/drawer/drawer.stories.tsx
================================================
import { Meta, StoryObj } from '@storybook/react';

import { Button } from '@/components/ui/button';
import { useDisclosure } from '@/hooks/use-disclosure';

import {
  Drawer,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
} from './drawer';

const meta: Meta<typeof Drawer> = {
  component: Drawer,
};

export default meta;

type Story = StoryObj<typeof Drawer>;

const DemoDrawer = () => {
  const { close, open, isOpen } = useDisclosure();

  return (
    <Drawer
      open={isOpen}
      onOpenChange={(isOpen) => {
        if (!isOpen) {
          close();
        } else {
          open();
        }
      }}
    >
      <DrawerTrigger asChild>
        <Button variant="outline">Open</Button>
      </DrawerTrigger>
      <DrawerContent className="flex max-w-[800px] flex-col justify-between sm:max-w-[540px]">
        <div className="flex flex-col">
          <DrawerHeader>
            <DrawerTitle>Drawer Header</DrawerTitle>
            <DrawerDescription>
              Lorem ipsum dolor sit amet, consectetur adipiscing elit.
            </DrawerDescription>
          </DrawerHeader>
          <div>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</div>
        </div>
        <DrawerFooter>
          <DrawerClose asChild>
            <Button type="submit">Save changes</Button>
          </DrawerClose>
        </DrawerFooter>
      </DrawerContent>
    </Drawer>
  );
};

export const Default: Story = {
  render: () => <DemoDrawer />,
};


================================================
FILE: apps/nextjs-app/src/components/ui/drawer/drawer.tsx
================================================
'use client';

import * as DrawerPrimitive from '@radix-ui/react-dialog';
import { Cross2Icon } from '@radix-ui/react-icons';
import { cva, type VariantProps } from 'class-variance-authority';
import * as React from 'react';

import { cn } from '@/utils/cn';

const Drawer = DrawerPrimitive.Root;

const DrawerTrigger = DrawerPrimitive.Trigger;

const DrawerClose = DrawerPrimitive.Close;

const DrawerPortal = DrawerPrimitive.Portal;

const DrawerOverlay = React.forwardRef<
  React.ElementRef<typeof DrawerPrimitive.Overlay>,
  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
>(({ className, ...props }, ref) => (
  <DrawerPrimitive.Overlay
    className={cn(
      'fixed inset-0 z-50 bg-black/80  data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
      className,
    )}
    {...props}
    ref={ref}
  />
));
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;

const drawerVariants = cva(
  'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out',
  {
    variants: {
      side: {
        top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
        bottom:
          'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
        left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
        right:
          'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm',
      },
    },
    defaultVariants: {
      side: 'right',
    },
  },
);

type DrawerContentProps = React.ComponentPropsWithoutRef<
  typeof DrawerPrimitive.Content
> &
  VariantProps<typeof drawerVariants>;

const DrawerContent = React.forwardRef<
  React.ElementRef<typeof DrawerPrimitive.Content>,
  DrawerContentProps
>(({ side = 'right', className, children, ...props }, ref) => (
  <DrawerPortal>
    <DrawerOverlay />
    <DrawerPrimitive.Content
      ref={ref}
      className={cn(drawerVariants({ side }), className)}
      {...props}
    >
      {children}
      <DrawerPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
        <Cross2Icon className="size-4" />
        <span className="sr-only">Close</span>
      </DrawerPrimitive.Close>
    </DrawerPrimitive.Content>
  </DrawerPortal>
));
DrawerContent.displayName = DrawerPrimitive.Content.displayName;

const DrawerHeader = ({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) => (
  <div
    className={cn(
      'flex flex-col space-y-2 text-center sm:text-left',
      className,
    )}
    {...props}
  />
);
DrawerHeader.displayName = 'DrawerHeader';

const DrawerFooter = ({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) => (
  <div
    className={cn(
      'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
      className,
    )}
    {...props}
  />
);
DrawerFooter.displayName = 'DrawerFooter';

const DrawerTitle = React.forwardRef<
  React.ElementRef<typeof DrawerPrimitive.Title>,
  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
>(({ className, ...props }, ref) => (
  <DrawerPrimitive.Title
    ref={ref}
    className={cn('text-lg font-semibold text-foreground', className)}
    {...props}
  />
));
DrawerTitle.displayName = DrawerPrimitive.Title.displayName;

const DrawerDescription = React.forwardRef<
  React.ElementRef<typeof DrawerPrimitive.Description>,
  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
>(({ className, ...props }, ref) => (
  <DrawerPrimitive.Description
    ref={ref}
    className={cn('text-sm text-muted-foreground', className)}
    {...props}
  />
));
DrawerDescription.displayName = DrawerPrimitive.Description.displayName;

export {
  Drawer,
  DrawerPortal,
  DrawerOverlay,
  DrawerTrigger,
  DrawerClose,
  DrawerContent,
  DrawerHeader,
  DrawerFooter,
  DrawerTitle,
  DrawerDescription,
};


================================================
FILE: apps/nextjs-app/src/components/ui/drawer/index.ts
================================================
export * from './drawer';


================================================
FILE: apps/nextjs-app/src/components/ui/dropdown/dropdown.stories.tsx
================================================
import type { Meta } from '@storybook/react';
import React from 'react';

import { Button } from '@/components/ui/button';

import {
  DropdownMenu,
  DropdownMenuTrigger,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuCheckboxItem,
  DropdownMenuRadioItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuSub,
  DropdownMenuSubTrigger,
  DropdownMenuSubContent,
  DropdownMenuRadioGroup,
} from './dropdown';

const meta: Meta = {
  component: DropdownMenu,
};

export default meta;

export const Default = () => (
  <DropdownMenu>
    <DropdownMenuTrigger asChild>
      <Button>Open Menu</Button>
    </DropdownMenuTrigger>
    <DropdownMenuContent>
      <DropdownMenuItem>Item One</DropdownMenuItem>
      <DropdownMenuItem>Item Two</DropdownMenuItem>
      <DropdownMenuSeparator />
      <DropdownMenuItem>Item Three</DropdownMenuItem>
    </DropdownMenuContent>
  </DropdownMenu>
);

export const WithCheckboxItems = () => {
  const [checked, setChecked] = React.useState(true);
  const [checked2, setChecked2] = React.useState(false);

  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button>Open Menu</Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent>
        <DropdownMenuCheckboxItem
          checked={checked}
          onCheckedChange={setChecked}
        >
          Option One
        </DropdownMenuCheckboxItem>
        <DropdownMenuCheckboxItem
          checked={checked2}
          onCheckedChange={setChecked2}
        >
          Option Two
        </DropdownMenuCheckboxItem>
      </DropdownMenuContent>
    </DropdownMenu>
  );
};

export const WithRadioItems = () => {
  const [value, setValue] = React.useState('one');

  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button>Open Menu</Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent>
        <DropdownMenuLabel>Select an option</DropdownMenuLabel>
        <DropdownMenuSeparator />
        <DropdownMenuRadioGroup value={value} onValueChange={setValue}>
          <DropdownMenuRadioItem value="one">Option One</DropdownMenuRadioItem>
          <DropdownMenuRadioItem value="two">Option Two</DropdownMenuRadioItem>
          <DropdownMenuRadioItem value="three">
            Option Three
          </DropdownMenuRadioItem>
        </DropdownMenuRadioGroup>
      </DropdownMenuContent>
    </DropdownMenu>
  );
};

export const WithSubmenus = () => (
  <DropdownMenu>
    <DropdownMenuTrigger>
      <Button>Open Menu</Button>
    </DropdownMenuTrigger>
    <DropdownMenuContent>
      <DropdownMenuItem>Item One</DropdownMenuItem>
      <DropdownMenuSub>
        <DropdownMenuSubTrigger>More Options</DropdownMenuSubTrigger>
        <DropdownMenuSubContent>
          <DropdownMenuItem>Sub Item One</DropdownMenuItem>
          <DropdownMenuItem>Sub Item Two</DropdownMenuItem>
        </DropdownMenuSubContent>
      </DropdownMenuSub>
      <DropdownMenuItem>Item Three</DropdownMenuItem>
    </DropdownMenuContent>
  </DropdownMenu>
);


================================================
FILE: apps/nextjs-app/src/components/ui/dropdown/dropdown.tsx
================================================
'use client';

import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
import {
  CheckIcon,
  ChevronRightIcon,
  DotFilledIcon,
} from '@radix-ui/react-icons';
import * as React from 'react';

import { cn } from '@/utils/cn';

const DropdownMenu = DropdownMenuPrimitive.Root;

const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;

const DropdownMenuGroup = DropdownMenuPrimitive.Group;

const DropdownMenuPortal = DropdownMenuPrimitive.Portal;

const DropdownMenuSub = DropdownMenuPrimitive.Sub;

const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;

const DropdownMenuSubTrigger = React.forwardRef<
  React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
    inset?: boolean;
  }
>(({ className, inset, children, ...props }, ref) => (
  <DropdownMenuPrimitive.SubTrigger
    ref={ref}
    className={cn(
      'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent',
      inset && 'pl-8',
      className,
    )}
    {...props}
  >
    {children}
    <ChevronRightIcon className="ml-auto size-4" />
  </DropdownMenuPrimitive.SubTrigger>
));
DropdownMenuSubTrigger.displayName =
  DropdownMenuPrimitive.SubTrigger.displayName;

const DropdownMenuSubContent = React.forwardRef<
  React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
  <DropdownMenuPrimitive.SubContent
    ref={ref}
    className={cn(
      'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
      className,
    )}
    {...props}
  />
));
DropdownMenuSubContent.displayName =
  DropdownMenuPrimitive.SubContent.displayName;

const DropdownMenuContent = React.forwardRef<
  React.ElementRef<typeof DropdownMenuPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
  <DropdownMenuPrimitive.Portal>
    <DropdownMenuPrimitive.Content
      ref={ref}
      sideOffset={sideOffset}
      className={cn(
        'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md',
        'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
        className,
      )}
      {...props}
    />
  </DropdownMenuPrimitive.Portal>
));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;

const DropdownMenuItem = React.forwardRef<
  React.ElementRef<typeof DropdownMenuPrimitive.Item>,
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
    inset?: boolean;
  }
>(({ className, inset, ...props }, ref) => (
  <DropdownMenuPrimitive.Item
    ref={ref}
    className={cn(
      'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
      inset && 'pl-8',
      className,
    )}
    {...props}
  />
));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;

const DropdownMenuCheckboxItem = React.forwardRef<
  React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
  <DropdownMenuPrimitive.CheckboxItem
    ref={ref}
    className={cn(
      'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
      className,
    )}
    checked={checked}
    {...props}
  >
    <span className="absolute left-2 flex size-3.5 items-center justify-center">
      <DropdownMenuPrimitive.ItemIndicator>
        <CheckIcon className="size-4" />
      </DropdownMenuPrimitive.ItemIndicator>
    </span>
    {children}
  </DropdownMenuPrimitive.CheckboxItem>
));
DropdownMenuCheckboxItem.displayName =
  DropdownMenuPrimitive.CheckboxItem.displayName;

const DropdownMenuRadioItem = React.forwardRef<
  React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
  <DropdownMenuPrimitive.RadioItem
    ref={ref}
    className={cn(
      'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
      className,
    )}
    {...props}
  >
    <span className="absolute left-2 flex size-3.5 items-center justify-center">
      <DropdownMenuPrimitive.ItemIndicator>
        <DotFilledIcon className="size-4 fill-current" />
      </DropdownMenuPrimitive.ItemIndicator>
    </span>
    {children}
  </DropdownMenuPrimitive.RadioItem>
));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;

const DropdownMenuLabel = React.forwardRef<
  React.ElementRef<typeof DropdownMenuPrimitive.Label>,
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
    inset?: boolean;
  }
>(({ className, inset, ...props }, ref) => (
  <DropdownMenuPrimitive.Label
    ref={ref}
    className={cn(
      'px-2 py-1.5 text-sm font-semibold',
      inset && 'pl-8',
      className,
    )}
    {...props}
  />
));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;

const DropdownMenuSeparator = React.forwardRef<
  React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
  <DropdownMenuPrimitive.Separator
    ref={ref}
    className={cn('-mx-1 my-1 h-px bg-muted', className)}
    {...props}
  />
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;

const DropdownMenuShortcut = ({
  className,
  ...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
  return (
    <span
      className={cn('ml-auto text-xs tracking-widest opacity-60', className)}
      {...props}
    />
  );
};
DropdownMenuShortcut.displayName = 'DropdownMenuShortcut';

export {
  DropdownMenu,
  DropdownMenuTrigger,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuCheckboxItem,
  DropdownMenuRadioItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuShortcut,
  DropdownMenuGroup,
  DropdownMenuPortal,
  DropdownMenuSub,
  DropdownMenuSubContent,
  DropdownMenuSubTrigger,
  DropdownMenuRadioGroup,
};


================================================
FILE: apps/nextjs-app/src/components/ui/dropdown/index.ts
================================================
export * from './dropdown';


================================================
FILE: apps/nextjs-app/src/components/ui/form/__tests__/form.test.tsx
================================================
import { SubmitHandler } from 'react-hook-form';
import { z } from 'zod';

import { Button } from '@/components/ui/button';
import { rtlRender, screen, waitFor, userEvent } from '@/testing/test-utils';

import { Form } from '../form';
import { Input } from '../input';

const testData = {
  title: 'Hello World',
};

const schema = z.object({
  title: z.string().min(1, 'Required'),
});

test('should render and submit a basic Form component', async () => {
  const handleSubmit = vi.fn() as SubmitHandler<z.infer<typeof schema>>;

  rtlRender(
    <Form onSubmit={handleSubmit} schema={schema} id="my-form">
      {({ register, formState }) => (
        <>
          <Input
            label="Title"
            error={formState.errors['title']}
            registration={register('title')}
          />

          <Button name="submit" type="submit" className="w-full">
            Submit
          </Button>
        </>
      )}
    </Form>,
  );

  await userEvent.type(screen.getByLabelText(/title/i), testData.title);

  await userEvent.click(screen.getByRole('button', { name: /submit/i }));

  await waitFor(() =>
    expect(handleSubmit).toHaveBeenCalledWith(testData, expect.anything()),
  );
});

test('should fail submission if validation fails', async () => {
  const handleSubmit = vi.fn() as SubmitHandler<z.infer<typeof schema>>;

  rtlRender(
    <Form onSubmit={handleSubmit} schema={schema} id="my-form">
      {({ register, formState }) => (
        <>
          <Input
            label="Title"
            error={formState.errors['title']}
            registration={register('title')}
          />

          <Button name="submit" type="submit" className="w-full">
            Submit
          </Button>
        </>
      )}
    </Form>,
  );

  await userEvent.click(screen.getByRole('button', { name: /submit/i }));

  await screen.findByRole('alert', { name: /required/i });

  expect(handleSubmit).toHaveBeenCalledTimes(0);
});


================================================
FILE: apps/nextjs-app/src/components/ui/form/error.tsx
================================================
export type ErrorProps = {
  errorMessage?: string | null;
};

export const Error = ({ errorMessage }: ErrorProps) => {
  if (!errorMessage) return null;

  return (
    <div
      role="alert"
      aria-label={errorMessage}
      className="text-sm font-semibold text-red-500"
    >
      {errorMessage}
    </div>
  );
};


================================================
FILE: apps/nextjs-app/src/components/ui/form/field-wrapper.tsx
================================================
import * as React from 'react';
import { type FieldError } from 'react-hook-form';

import { Error } from './error';
import { Label } from './label';

type FieldWrapperProps = {
  label?: string;
  className?: string;
  children: React.ReactNode;
  error?: FieldError | undefined;
};

export type FieldWrapperPassThroughProps = Omit<
  FieldWrapperProps,
  'className' | 'children'
>;

export const FieldWrapper = (props: FieldWrapperProps) => {
  const { label, error, children } = props;
  return (
    <div>
      <Label>
        {label}
        <div className="mt-1">{children}</div>
      </Label>
      <Error errorMessage={error?.message} />
    </div>
  );
};


================================================
FILE: apps/nextjs-app/src/components/ui/form/form-drawer.tsx
================================================
'use client';

import * as React from 'react';

import { useDisclosure } from '@/hooks/use-disclosure';

import { Button } from '../button';
import {
  Drawer,
  DrawerClose,
  DrawerContent,
  DrawerFooter,
  DrawerHeader,
  DrawerTrigger,
  DrawerTitle,
} from '../drawer';

type FormDrawerProps = {
  isDone: boolean;
  triggerButton: React.ReactElement;
  submitButton: React.ReactElement;
  title: string;
  children: React.ReactNode;
};

export const FormDrawer = ({
  title,
  children,
  isDone,
  triggerButton,
  submitButton,
}: FormDrawerProps) => {
  const { close, open, isOpen } = useDisclosure();

  React.useEffect(() => {
    if (isDone) {
      close();
    }
  }, [isDone, close]);

  return (
    <Drawer
      open={isOpen}
      onOpenChange={(isOpen) => {
        if (!isOpen) {
          close();
        } else {
          open();
        }
      }}
    >
      <DrawerTrigger asChild>{triggerButton}</DrawerTrigger>
      <DrawerContent className="flex max-w-[800px] flex-col justify-between sm:max-w-[540px]">
        <div className="flex flex-col">
          <DrawerHeader>
            <DrawerTitle>{title}</DrawerTitle>
          </DrawerHeader>
          <div>{children}</div>
        </div>
        <DrawerFooter>
          <DrawerClose asChild>
            <Button variant="outline" type="submit">
              Close
            </Button>
          </DrawerClose>
          {submitButton}
        </DrawerFooter>
      </DrawerContent>
    </Drawer>
  );
};


================================================
FILE: apps/nextjs-app/src/components/ui/form/form.stories.tsx
================================================
import { Meta, StoryObj } from '@storybook/react';
import { z } from 'zod';

import { Button } from '../button';

import { Form } from './form';
import { FormDrawer } from './form-drawer';
import { Input } from './input';
import { Select } from './select';
import { Textarea } from './textarea';

const MyForm = ({ hideSubmit = false }: { hideSubmit?: boolean }) => {
  return (
    <Form
      onSubmit={async (values) => {
        alert(JSON.stringify(values, null, 2));
      }}
      schema={z.object({
        title: z.string().min(1, 'Required'),
        description: z.string().min(1, 'Required'),
        type: z.string().min(1, 'Required'),
      })}
      id="my-form"
    >
      {({ register, formState }) => (
        <>
          <Input
            label="Title"
            error={formState.errors['title']}
            registration={register('title')}
          />
          <Textarea
            label="Description"
            error={formState.errors['description']}
            registration={register('description')}
          />
          <Select
            label="Type"
            error={formState.errors['type']}
            registration={register('type')}
            options={['A', 'B', 'C'].map((type) => ({
              label: type,
              value: type,
            }))}
          />

          {!hideSubmit && (
            <div>
              <Button type="submit" className="w-full">
                Submit
              </Button>
            </div>
          )}
        </>
      )}
    </Form>
  );
};

const meta: Meta = {
  component: MyForm,
};

export default meta;

type Story = StoryObj<typeof MyForm>;

export const Default: Story = {
  render: () => <MyForm />,
};

export const AsFormDrawer: Story = {
  render: () => (
    <FormDrawer
      triggerButton={<Button>Open Form</Button>}
      isDone={true}
      title="My Form"
      submitButton={
        <Button form="my-form" type="submit">
          Submit
        </Button>
      }
    >
      <MyForm hideSubmit />
    </FormDrawer>
  ),
};


================================================
FILE: apps/nextjs-app/src/components/ui/form/form.tsx
================================================
'use client';

import { zodResolver } from '@hookform/resolvers/zod';
import * as LabelPrimitive from '@radix-ui/react-label';
import { Slot } from '@radix-ui/react-slot';
import * as React from 'react';
import {
  Controller,
  ControllerProps,
  FieldPath,
  FieldValues,
  FormProvider,
  SubmitHandler,
  UseFormProps,
  UseFormReturn,
  useForm,
  useFormContext,
} from 'react-hook-form';
import { ZodType, z } from 'zod';

import { cn } from '@/utils/cn';

import { Label } from './label';

type FormFieldContextValue<
  TFieldValues extends FieldValues = FieldValues,
  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
  name: TName;
};

const FormFieldContext = React.createContext<FormFieldContextValue>(
  {} as FormFieldContextValue,
);

const FormField = <
  TFieldValues extends FieldValues,
  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
  ...props
}: ControllerProps<TFieldValues, TName>) => {
  return (
    <FormFieldContext.Provider value={{ name: props.name }}>
      <Controller {...props} />
    </FormFieldContext.Provider>
  );
};

const useFormField = () => {
  const fieldContext = React.useContext(FormFieldContext);
  const itemContext = React.useContext(FormItemContext);
  const { getFieldState, formState } = useFormContext();

  const fieldState = getFieldState(fieldContext.name, formState);

  if (!fieldContext) {
    throw new Error('useFormField should be used within <FormField>');
  }

  const { id } = itemContext;

  return {
    id,
    name: fieldContext.name,
    formItemId: `${id}-form-item`,
    formDescriptionId: `${id}-form-item-description`,
    formMessageId: `${id}-form-item-message`,
    ...fieldState,
  };
};

type FormItemContextValue = {
  id: string;
};

const FormItemContext = React.createContext<FormItemContextValue>(
  {} as FormItemContextValue,
);

const FormItem = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
  const id = React.useId();

  return (
    <FormItemContext.Provider value={{ id }}>
      <div ref={ref} className={cn('space-y-2', className)} {...props} />
    </FormItemContext.Provider>
  );
});
FormItem.displayName = 'FormItem';

const FormLabel = React.forwardRef<
  React.ElementRef<typeof LabelPrimitive.Root>,
  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
  const { error, formItemId } = useFormField();

  return (
    <Label
      ref={ref}
      className={cn(error && 'text-destructive', className)}
      htmlFor={formItemId}
      {...props}
    />
  );
});
FormLabel.displayName = 'FormLabel';

const FormControl = React.forwardRef<
  React.ElementRef<typeof Slot>,
  React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
  const { error, formItemId, formDescriptionId, formMessageId } =
    useFormField();

  return (
    <Slot
      ref={ref}
      id={formItemId}
      aria-describedby={
        !error
          ? `${formDescriptionId}`
          : `${formDescriptionId} ${formMessageId}`
      }
      aria-invalid={!!error}
      {...props}
    />
  );
});
FormControl.displayName = 'FormControl';

const FormDescription = React.forwardRef<
  HTMLParagraphElement,
  React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
  const { formDescriptionId } = useFormField();

  return (
    <p
      ref={ref}
      id={formDescriptionId}
      className={cn('text-[0.8rem] text-muted-foreground', className)}
      {...props}
    />
  );
});
FormDescription.displayName = 'FormDescription';

const FormMessage = React.forwardRef<
  HTMLParagraphElement,
  React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
  const { error, formMessageId } = useFormField();
  const body = error ? String(error?.message) : children;

  if (!body) {
    return null;
  }

  return (
    <p
      ref={ref}
      id={formMessageId}
      className={cn('text-[0.8rem] font-medium text-destructive', className)}
      {...props}
    >
      {body}
    </p>
  );
});
FormMessage.displayName = 'FormMessage';

type FormProps<TFormValues extends FieldValues, Schema> = {
  onSubmit: SubmitHandler<TFormValues>;
  schema: Schema;
  className?: string;
  children: (methods: UseFormReturn<TFormValues>) => React.ReactNode;
  options?: UseFormProps<TFormValues>;
  id?: string;
};

const Form = <
  Schema extends ZodType<any, any, any>,
  TFormValues extends FieldValues = z.infer<Schema>,
>({
  onSubmit,
  children,
  className,
  options,
  id,
  schema,
}: FormProps<TFormValues, Schema>) => {
  const form = useForm({ ...options, resolver: zodResolver(schema) });
  return (
    <FormProvider {...form}>
      <form
        className={cn('space-y-6', className)}
        onSubmit={form.handleSubmit(onSubmit)}
        id={id}
      >
        {children(form)}
      </form>
    </FormProvider>
  );
};

export {
  useFormField,
  Form,
  FormProvider,
  FormItem,
  FormLabel,
  FormControl,
  FormDescription,
  FormMessage,
  FormField,
};


================================================
FILE: apps/nextjs-app/src/components/ui/form/index.ts
================================================
export * from './form';
export * from './input';
export * from './select';
export * from './textarea';
export * from './form-drawer';
export * from './label';
export * from './switch';


================================================
FILE: apps/nextjs-app/src/components/ui/form/input.tsx
================================================
import * as React from 'react';
import { type UseFormRegisterReturn } from 'react-hook-form';

import { cn } from '@/utils/cn';

import { FieldWrapper, FieldWrapperPassThroughProps } from './field-wrapper';

export type InputProps = React.InputHTMLAttributes<HTMLInputElement> &
  FieldWrapperPassThroughProps & {
    className?: string;
    registration: Partial<UseFormRegisterReturn>;
  };

const Input = React.forwardRef<HTMLInputElement, InputProps>(
  ({ className, type, label, error, registration, ...props }, ref) => {
    return (
      <FieldWrapper label={label} error={error}>
        <input
          type={type}
          className={cn(
            'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
            className,
          )}
          ref={ref}
          {...registration}
          {...props}
        />
      </FieldWrapper>
    );
  },
);
Input.displayName = 'Input';

export { Input };


================================================
FILE: apps/nextjs-app/src/components/ui/form/label.tsx
================================================
'use client';

import * as LabelPrimitive from '@radix-ui/react-label';
import { cva, type VariantProps } from 'class-variance-authority';
import * as React from 'react';

import { cn } from '@/utils/cn';

const labelVariants = cva(
  'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
);

const Label = React.forwardRef<
  React.ElementRef<typeof LabelPrimitive.Root>,
  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
    VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
  <LabelPrimitive.Root
    ref={ref}
    className={cn(labelVariants(), className)}
    {...props}
  />
));
Label.displayName = LabelPrimitive.Root.displayName;

export { Label };


================================================
FILE: apps/nextjs-app/src/components/ui/form/select.tsx
================================================
'use client';

import * as React from 'react';
import { UseFormRegisterReturn } from 'react-hook-form';

import { cn } from '@/utils/cn';

import { FieldWrapper, FieldWrapperPassThroughProps } from './field-wrapper';

type Option = {
  label: React.ReactNode;
  value: string | number | string[];
};

type SelectFieldProps = FieldWrapperPassThroughProps & {
  options: Option[];
  className?: string;
  defaultValue?: string;
  registration: Partial<UseFormRegisterReturn>;
};

export const Select = (props: SelectFieldProps) => {
  const { label, options, error, className, defaultValue, registration } =
    props;
  return (
    <FieldWrapper label={label} error={error}>
      <select
        className={cn(
          'mt-1 block w-full rounded-md border-gray-600 py-2 pl-3 pr-10 text-base focus:border-blue-500 focus:outline-none focus:ring-blue-500 sm:text-sm',
          className,
        )}
        defaultValue={defaultValue}
        {...registration}
      >
        {options.map(({ label, value }) => (
          <option key={label?.toString()} value={value}>
            {label}
          </option>
        ))}
      </select>
    </FieldWrapper>
  );
};


================================================
FILE: apps/nextjs-app/src/components/ui/form/switch.tsx
================================================
'use client';

import * as SwitchPrimitives from '@radix-ui/react-switch';
import * as React from 'react';

import { cn } from '@/utils/cn';

const Switch = React.forwardRef<
  React.ElementRef<typeof SwitchPrimitives.Root>,
  React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
  <SwitchPrimitives.Root
    className={cn(
      'peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
      className,
    )}
    {...props}
    ref={ref}
  >
    <SwitchPrimitives.Thumb
      className={cn(
        'pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0',
      )}
    />
  </SwitchPrimitives.Root>
));
Switch.displayName = SwitchPrimitives.Root.displayName;

export { Switch };


================================================
FILE: apps/nextjs-app/src/components/ui/form/textarea.tsx
================================================
import * as React from 'react';
import { UseFormRegisterReturn } from 'react-hook-form';

import { cn } from '@/utils/cn';

import { FieldWrapper, FieldWrapperPassThroughProps } from './field-wrapper';

export type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement> &
  FieldWrapperPassThroughProps & {
    className?: string;
    registration: Partial<UseFormRegisterReturn>;
  };

const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
  ({ className, label, error, registration, ...props }, ref) => {
    return (
      <FieldWrapper label={label} error={error}>
        <textarea
          className={cn(
            'flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
            className,
          )}
          ref={ref}
          {...registration}
          {...props}
        />
      </FieldWrapper>
    );
  },
);
Textarea.displayName = 'Textarea';

export { Textarea };


================================================
FILE: apps/nextjs-app/src/components/ui/link/index.ts
================================================
export * from './link';


================================================
FILE: apps/nextjs-app/src/components/ui/link/link.stories.tsx
================================================
import { Meta, StoryObj } from '@storybook/react';

import { Link } from './link';

const meta: Meta<typeof Link> = {
  component: Link,
};

export default meta;

type Story = StoryObj<typeof Link>;

export const Default: Story = {
  args: {
    children: 'Link',
    href: '/',
  },
};


================================================
FILE: apps/nextjs-app/src/components/ui/link/link.tsx
================================================
import NextLink, { LinkProps as NextLinkProps } from 'next/link';

import { cn } from '@/utils/cn';

export type LinkProps = {
  className?: string;
  children: React.ReactNode;
  target?: string;
} & NextLinkProps;

export const Link = ({ className, children, href, ...props }: LinkProps) => {
  return (
    <NextLink
      href={href}
      className={cn('text-slate-600 hover:text-slate-900', className)}
      {...props}
    >
      {children}
    </NextLink>
  );
};


================================================
FILE: apps/nextjs-app/src/components/ui/md-preview/index.ts
================================================
export * from './md-preview';


================================================
FILE: apps/nextjs-app/src/components/ui/md-preview/md-preview.stories.tsx
================================================
import { Meta, StoryObj } from '@storybook/react';

import { MDPreview } from './md-preview';

const meta: Meta<typeof MDPreview> = {
  component: MDPreview,
};

export default meta;

type Story = StoryObj<typeof MDPreview>;

export const Default: Story = {
  args: {
    value: `## Hello World!`,
  },
};


================================================
FILE: apps/nextjs-app/src/components/ui/md-preview/md-preview.tsx
================================================
import DOMPurify from 'isomorphic-dompurify';
import { parse } from 'marked';

export type MDPreviewProps = {
  value: string;
};

export const MDPreview = ({ value = '' }: MDPreviewProps) => {
  return (
    <div
      className="prose prose-slate w-full p-2"
      dangerouslySetInnerHTML={{
        __html: DOMPurify.sanitize(parse(value) as string),
      }}
    />
  );
};


================================================
FILE: apps/nextjs-app/src/components/ui/notifications/__tests__/notifications.test.ts
================================================
import { renderHook, act } from '@testing-library/react';

import { useNotifications, Notification } from '../notifications-store';

test('should add and remove notifications', () => {
  const { result } = renderHook(() => useNotifications());

  expect(result.current.notifications.length).toBe(0);

  const notification: Notification = {
    id: '123',
    title: 'Hello World',
    type: 'info',
    message: 'This is a notification',
  };

  act(() => {
    result.current.addNotification(notification);
  });

  expect(result.current.notifications).toContainEqual(notification);

  act(() => {
    result.current.dismissNotification(notification.id);
  });

  expect(result.current.notifications).not.toContainEqual(notification);
});


================================================
FILE: apps/nextjs-app/src/components/ui/notifications/index.ts
================================================
export * from './notifications';
export * from './notifications-store';


================================================
FILE: apps/nextjs-app/src/components/ui/notifications/notification.stories.tsx
================================================
import { Meta, StoryObj } from '@storybook/react';

import { Notification } from './notification';

const meta: Meta<typeof Notification> = {
  title: 'Components/Notifications',
  component: Notification,
  parameters: {
    controls: { expanded: true },
  },
};

export default meta;

type Story = StoryObj<typeof Notification>;

export const Info: Story = {
  args: {
    notification: {
      id: '1',
      type: 'info',
      title: 'Hello Info',
      message: 'This is info notification',
    },
    onDismiss: (id) => alert(`Dismissing Notification with id: ${id}`),
  },
};

export const Success: Story = {
  args: {
    notification: {
      id: '1',
      type: 'success',
      title: 'Hello Success',
      message: 'This is success notification',
    },
    onDismiss: (id) => alert(`Dismissing Notification with id: ${id}`),
  },
};

export const Warning: Story = {
  args: {
    notification: {
      id: '1',
      type: 'warning',
      title: 'Hello Warning',
      message: 'This is warning notification',
    },
    onDismiss: (id) => alert(`Dismissing Notification with id: ${id}`),
  },
};

export const Error: Story = {
  args: {
    notification: {
      id: '1',
      type: 'error',
      title: 'Hello Error',
      message: 'This is error notification',
    },
    onDismiss: (id) => alert(`Dismissing Notification with id: ${id}`),
  },
};


================================================
FILE: apps/nextjs-app/src/components/ui/notifications/notification.tsx
================================================
'use client';

import { Info, CircleAlert, CircleX, CircleCheck } from 'lucide-react';

const icons = {
  info: <Info className="size-6 text-blue-500" aria-hidden="true" />,
  success: <CircleCheck className="size-6 text-green-500" aria-hidden="true" />,
  warning: (
    <CircleAlert className="size-6 text-yellow-500" aria-hidden="true" />
  ),
  error: <CircleX className="size-6 text-red-500" aria-hidden="true" />,
};

export type NotificationProps = {
  notification: {
    id: string;
    type: keyof typeof icons;
    title: string;
    message?: string;
  };
  onDismiss: (id: string) => void;
};

export const Notification = ({
  notification: { id, type, title, message },
  onDismiss,
}: NotificationProps) => {
  return (
    <div className="flex w-full flex-col items-center space-y-4 sm:items-end">
      <div className="pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg bg-white shadow-lg ring-1 ring-black/5">
        <div className="p-4" role="alert" aria-label={title}>
          <div className="flex items-start">
            <div className="shrink-0">{icons[type]}</div>
            <div className="ml-3 w-0 flex-1 pt-0.5">
              <p className="text-sm font-medium text-gray-900">{title}</p>
              <p className="mt-1 text-sm text-gray-500">{message}</p>
            </div>
            <div className="ml-4 flex shrink-0">
              <button
                className="inline-flex rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-2"
                onClick={() => {
                  onDismiss(id);
                }}
              >
                <span className="sr-only">Close</span>
                <CircleX className="size-5" aria-hidden="true" />
              </button>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
};


================================================
FILE: apps/nextjs-app/src/components/ui/notifications/notifications-store.ts
================================================
import { nanoid } from 'nanoid';
import { create } from 'zustand';

export type Notification = {
  id: string;
  type: 'info' | 'warning' | 'success' | 'error';
  title: string;
  message?: string;
};

type NotificationsStore = {
  notifications: Notification[];
  addNotification: (notification: Omit<Notification, 'id'>) => void;
  dismissNotification: (id: string) => void;
};

export const useNotifications = create<NotificationsStore>((set) => ({
  notifications: [],
  addNotification: (notification) =>
    set((state) => ({
      notifications: [
        ...state.notifications,
        { id: nanoid(), ...notification },
      ],
    })),
  dismissNotification: (id) =>
    set((state) => ({
      notifications: state.notifications.filter(
        (notification) => notification.id !== id,
      ),
    })),
}));


================================================
FILE: apps/nextjs-app/src/components/ui/notifications/notifications.tsx
================================================
'use client';

import { Notification } from './notification';
import { useNotifications } from './notifications-store';

export const Notifications = () => {
  const { notifications, dismissNotification } = useNotifications();

  return (
    <div
      aria-live="assertive"
      className="pointer-events-none fixed inset-0 z-50 flex flex-col items-end space-y-4 px-4 py-6 sm:items-start sm:p-6"
    >
      {notifications.map((notification) => (
        <Notification
          key={notification.id}
          notification={notification}
          onDismiss={dismissNotification}
        />
      ))}
    </div>
  );
};


================================================
FILE: apps/nextjs-app/src/components/ui/spinner/index.ts
================================================
export * from './spinner';


================================================
FILE: apps/nextjs-app/src/components/ui/spinner/spinner.stories.tsx
================================================
import { Meta, StoryObj } from '@storybook/react';

import { Spinner } from './spinner';

const meta: Meta<typeof Spinner> = {
  component: Spinner,
};

export default meta;

type Story = StoryObj<typeof Spinner>;

export const Default: Story = {
  args: {
    size: 'md',
  },
};


================================================
FILE: apps/nextjs-app/src/components/ui/spinner/spinner.tsx
================================================
import { cn } from '@/utils/cn';

const sizes = {
  sm: 'h-4 w-4',
  md: 'h-8 w-8',
  lg: 'h-16 w-16',
  xl: 'h-24 w-24',
};

const variants = {
  light: 'text-white',
  primary: 'text-slate-600',
};

export type SpinnerProps = {
  size?: keyof typeof sizes;
  variant?: keyof typeof variants;
  className?: string;
};

export const Spinner = ({
  size = 'md',
  variant = 'primary',
  className = '',
}: SpinnerProps) => {
  return (
    <>
      <svg
        xmlns="http://www.w3.org/2000/svg"
        width="24"
        height="24"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        strokeWidth="2"
        strokeLinecap="round"
        strokeLinejoin="round"
        className={cn(
          'animate-spin',
          sizes[size],
          variants[variant],
          className,
        )}
      >
        <path d="M21 12a9 9 0 1 1-6.219-8.56" />
      </svg>
      <span className="sr-only">Loading</span>
    </>
  );
};


================================================
FILE: apps/nextjs-app/src/components/ui/table/index.ts
================================================
export * from './table';


================================================
FILE: apps/nextjs-app/src/components/ui/table/pagination.tsx
================================================
import {
  ChevronLeftIcon,
  ChevronRightIcon,
  DotsHorizontalIcon,
} from '@radix-ui/react-icons';
import * as React from 'react';

import { ButtonProps, buttonVariants } from '@/components/ui/button';
import { cn } from '@/utils/cn';

import { Link } from '../link';

const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => (
  <nav
    role="navigation"
    aria-label="pagination"
    className={cn('mx-auto flex w-full justify-center', className)}
    {...props}
  />
);
Pagination.displayName = 'Pagination';

const PaginationContent = React.forwardRef<
  HTMLUListElement,
  React.ComponentProps<'ul'>
>(({ className, ...props }, ref) => (
  <ul
    ref={ref}
    className={cn('flex flex-row items-center gap-1', className)}
    {...props}
  />
));
PaginationContent.displayName = 'PaginationContent';

const PaginationItem = React.forwardRef<
  HTMLLIElement,
  React.ComponentProps<'li'>
>(({ className, ...props }, ref) => (
  <li ref={ref} className={cn('', className)} {...props} />
));
PaginationItem.displayName = 'PaginationItem';

type PaginationLinkProps = {
  isActive?: boolean;
} & Pick<ButtonProps, 'size'> &
  React.ComponentProps<'a'>;

const PaginationLink = ({
  className,
  isActive,
  size = 'icon',
  children,
  href,
  ...props
}: PaginationLinkProps) => (
  <Link
    href={href as string}
    aria-current={isActive ? 'page' : undefined}
    className={cn(
      buttonVariants({
        variant: isActive ? 'outline' : 'ghost',
        size,
      }),
      className,
    )}
    {...props}
  >
    {children}
  </Link>
);
PaginationLink.displayName = 'PaginationLink';

const PaginationPrevious = ({
  className,
  ...props
}: React.ComponentProps<typeof PaginationLink>) => (
  <PaginationLink
    aria-label="Go to previous page"
    size="default"
    className={cn('gap-1 pl-2.5', className)}
    {...props}
  >
    <ChevronLeftIcon className="size-4" />
    <span>Previous</span>
  </PaginationLink>
);
PaginationPrevious.displayName = 'PaginationPrevious';

const PaginationNext = ({
  className,
  ...props
}: React.ComponentProps<typeof PaginationLink>) => (
  <PaginationLink
    aria-label="Go to next page"
    size="default"
    className={cn('gap-1 pr-2.5', className)}
    {...props}
  >
    <span>Next</span>
    <ChevronRightIcon className="size-4" />
  </PaginationLink>
);
PaginationNext.displayName = 'PaginationNext';

const PaginationEllipsis = ({
  className,
  ...props
}: React.ComponentProps<'span'>) => (
  <span
    aria-hidden
    className={cn('flex h-9 w-9 items-center justify-center', className)}
    {...props}
  >
    <DotsHorizontalIcon className="size-4" />
    <span className="sr-only">More pages</span>
  </span>
);
PaginationEllipsis.displayName = 'PaginationEllipsis';

export {
  Pagination,
  PaginationContent,
  PaginationLink,
  PaginationItem,
  PaginationPrevious,
  PaginationNext,
  PaginationEllipsis,
};

export type TablePaginationProps = {
  totalPages: number;
  currentPage: number;
  rootUrl: string;
};

export const TablePagination = ({
  totalPages,
  currentPage,
  rootUrl,
}: TablePaginationProps) => {
  const createHref = (page: number) => `${rootUrl}?page=${page}`;

  return (
    <Pagination className="justify-end py-8">
      <PaginationContent>
        {currentPage > 1 && (
          <PaginationItem>
            <PaginationPrevious href={createHref(currentPage - 1)} />
          </PaginationItem>
        )}
        {currentPage > 2 && (
          <PaginationItem>
            <PaginationEllipsis />
          </PaginationItem>
        )}
        {currentPage > 1 && (
          <PaginationItem>
            <PaginationLink href={createHref(currentPage - 1)}>
              {currentPage - 1}
            </PaginationLink>
          </PaginationItem>
        )}
        <PaginationItem className="rounded-sm bg-gray-200">
          <PaginationLink href={createHref(currentPage)}>
            {currentPage}
          </PaginationLink>
        </PaginationItem>
        {totalPages > currentPage && (
          <PaginationItem>
            <PaginationLink href={createHref(currentPage + 1)}>
              {currentPage + 1}
            </PaginationLink>
          </PaginationItem>
        )}
        {totalPages > currentPage + 1 && (
          <PaginationItem>
            <PaginationEllipsis />
          </PaginationItem>
        )}
        {currentPage < totalPages && (
          <PaginationItem>
            <PaginationNext href={createHref(totalPages)} />
          </PaginationItem>
        )}
      </PaginationContent>
    </Pagination>
  );
};


================================================
FILE: apps/nextjs-app/src/components/ui/table/table.stories.tsx
================================================
import { Meta, StoryObj } from '@storybook/react';

import { Table } from './table';

const meta: Meta<typeof Table> = {
  component: Table,
};

export default meta;

type User = {
  id: string;
  createdAt: number;
  name: string;
  title: string;
  role: string;
  email: string;
};

type Story = StoryObj<typeof Table<User>>;

const data: User[] = [
  {
    id: '1',
    createdAt: Date.now(),
    name: 'Jane Cooper',
    title: 'Regional Paradigm Technician',
    role: 'Admin',
    email: 'jane.cooper@example.com',
  },
  {
    id: '2',
    createdAt: Date.now(),
    name: 'Cody Fisher',
    title: 'Product Directives Officer',
    role: 'Owner',
    email: 'cody.fisher@example.com',
  },
];

export const Default: Story = {
  args: {
    data,
    columns: [
      {
        title: 'Name',
        field: 'name',
      },
      {
        title: 'Title',
        field: 'title',
      },
      {
        title: 'Role',
        field: 'role',
      },
      {
        title: 'Email',
        field: 'email',
      },
    ],
  },
};


================================================
FILE: apps/nextjs-app/src/components/ui/table/table.tsx
================================================
import { ArchiveX } from 'lucide-react';
import * as React from 'react';

import { BaseEntity } from '@/types/api';
import { cn } from '@/utils/cn';

import { TablePagination, TablePaginationProps } from './pagination';

const TableElement = React.forwardRef<
  HTMLTableElement,
  React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
  <div className="relative w-full overflow-auto">
    <table
      ref={ref}
      className={cn('w-full caption-bottom text-sm', className)}
      {...props}
    />
  </div>
));
TableElement.displayName = 'Table';

const TableHeader = React.forwardRef<
  HTMLTableSectionElement,
  React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
  <thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />
));
TableHeader.displayName = 'TableHeader';

const TableBody = React.forwardRef<
  HTMLTableSectionElement,
  React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
  <tbody
    ref={ref}
    className={cn('[&_tr:last-child]:border-0', className)}
    {...props}
  />
));
TableBody.displayName = 'TableBody';

const TableFooter = React.forwardRef<
  HTMLTableSectionElement,
  React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
  <tfoot
    ref={ref}
    className={cn(
      'border-t bg-muted/50 font-medium [&>tr]:last:border-b-0',
      className,
    )}
    {...props}
  />
));
TableFooter.displayName = 'TableFooter';

const TableRow = React.forwardRef<
  HTMLTableRowElement,
  React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
  <tr
    ref={ref}
    className={cn(
      'border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted',
      className,
    )}
    {...props}
  />
));
TableRow.displayName = 'TableRow';

const TableHead = React.forwardRef<
  HTMLTableCellElement,
  React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
  <th
    ref={ref}
    className={cn(
      'h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
      className,
    )}
    {...props}
  />
));
TableHead.displayName = 'TableHead';

const TableCell = React.forwardRef<
  HTMLTableCellElement,
  React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
  <td
    ref={ref}
    className={cn(
      'p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
      className,
    )}
    {...props}
  />
));
TableCell.displayName = 'TableCell';

const TableCaption = React.forwardRef<
  HTMLTableCaptionElement,
  React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
  <caption
    ref={ref}
    className={cn('mt-4 text-sm text-muted-foreground', className)}
    {...props}
  />
));
TableCaption.displayName = 'TableCaption';

export {
  TableElement,
  TableHeader,
  TableBody,
  TableFooter,
  TableHead,
  TableRow,
  TableCell,
  TableCaption,
};

type TableColumn<Entry> = {
  title: string;
  field: keyof Entry;
  Cell?({ entry }: { entry: Entry }): React.ReactElement;
};

export type TableProps<Entry> = {
  data: Entry[];
  columns: TableColumn<Entry>[];
  pagination?: TablePaginationProps;
};

export const Table = <Entry extends BaseEntity>({
  data,
  columns,
  pagination,
}: TableProps<Entry>) => {
  if (!data?.length) {
    return (
      <div className="flex h-80 flex-col items-center justify-center bg-white text-gray-500">
        <ArchiveX className="size-16" />
        <h4>No Entries Found</h4>
      </div>
    );
  }
  return (
    <>
      <TableElement>
        <TableHeader>
          <TableRow>
            {columns.map((column, index) => (
              <TableHead key={column.title + index}>{column.title}</TableHead>
            ))}
          </TableRow>
        </TableHeader>
        <TableBody>
          {data.map((entry, entryIndex) => (
            <TableRow key={entry?.id || entryIndex}>
              {columns.map(({ Cell, field, title }, columnIndex) => (
                <TableCell key={title + columnIndex}>
                  {Cell ? <Cell entry={entry} /> : `${entry[field]}`}
                </TableCell>
              ))}
            </TableRow>
          ))}
        </TableBody>
      </TableElement>

      {pagination && <TablePagination {...pagination} />}
    </>
  );
};


================================================
FILE: apps/nextjs-app/src/config/env.ts
================================================
import * as z from 'zod';
import 'dotenv/config';

const createEnv = () => {
  const EnvSchema = z.object({
    API_URL: z.string(),
    ENABLE_API_MOCKING: z
      .string()
      .refine((s) => s === 'true' || s === 'false')
      .transform((s) => s === 'true')
      .optional(),
    APP_URL: z.string().optional().default('http://localhost:3000'),
    APP_MOCK_API_PORT: z.string().optional().default('8080'),
  });

  const envVars = {
    API_URL: process.env.NEXT_PUBLIC_API_URL,
    ENABLE_API_MOCKING: process.env.NEXT_PUBLIC_ENABLE_API_MOCKING,
    APP_URL: process.env.NEXT_PUBLIC_URL,
    APP_MOCK_API_PORT: process.env.NEXT_PUBLIC_MOCK_API_PORT,
  };

  const parsedEnv = EnvSchema.safeParse(envVars);

  if (!parsedEnv.success) {
    throw new Error(
      `Invalid env provided.
  The following variables are missing or invalid:
  ${Object.entries(parsedEnv.error.flatten().fieldErrors)
    .map(([k, v]) => `- ${k}: ${v}`)
    .join('\n')}
  `,
    );
  }

  return parsedEnv.data ?? {};
};

export const env = createEnv();


================================================
FILE: apps/nextjs-app/src/config/paths.ts
================================================
export const paths = {
  home: {
    getHref: () => '/',
  },

  auth: {
    register: {
      getHref: (redirectTo?: string | null | undefined) =>
        `/auth/register${redirectTo ? `?redirectTo=${encodeURIComponent(redirectTo)}` : ''}`,
    },
    login: {
      getHref: (redirectTo?: string | null | undefined) =>
        `/auth/login${redirectTo ? `?redirectTo=${encodeURIComponent(redirectTo)}` : ''}`,
    },
  },

  app: {
    root: {
      getHref: () => '/app',
    },
    dashboard: {
      getHref: () => '/app',
    },
    discussions: {
      getHref: () => '/app/discussions',
    },
    discussion: {
      getHref: (id: string) => `/app/discussions/${id}`,
    },
    users: {
      getHref: () => '/app/users',
    },
    profile: {
      getHref: () => '/app/profile',
    },
  },
  public: {
    discussion: {
      getHref: (id: string) => `/public/discussions/${id}`,
    },
  },
} as const;


================================================
FILE: apps/nextjs-app/src/features/auth/components/__tests__/login-form.test.tsx
================================================
import {
  createUser,
  renderApp,
  screen,
  userEvent,
  waitFor,
} from '@/testing/test-utils';

import { LoginForm } from '../login-form';

test('should login new user and call onSuccess cb which should navigate the user to the app', async () => {
  const newUser = await createUser({ teamId: undefined });

  const onSuccess = vi.fn();

  await renderApp(<LoginForm onSuccess={onSuccess} />, { user: null });

  await userEvent.type(screen.getByLabelText(/email address/i), newUser.email);
  await userEvent.type(screen.getByLabelText(/password/i), newUser.password);

  await userEvent.click(screen.getByRole('button', { name: /log in/i }));

  await waitFor(() => expect(onSuccess).toHaveBeenCalledTimes(1));
});


================================================
FILE: apps/nextjs-app/src/features/auth/components/__tests__/register-form.test.tsx
================================================
import { createUser } from '@/testing/data-generators';
import { renderApp, screen, userEvent, waitFor } from '@/testing/test-utils';

import { RegisterForm } from '../register-form';

test('should register new user and call onSuccess cb which should navigate the user to the app', async () => {
  const newUser = createUser({});

  const onSuccess = vi.fn();

  await renderApp(
    <RegisterForm
      onSuccess={onSuccess}
      chooseTeam={false}
      setChooseTeam={() => {}}
      teams={[]}
    />,
    { user: null },
  );

  await userEvent.type(screen.getByLabelText(/first name/i), newUser.firstName);
  await userEvent.type(screen.getByLabelText(/last name/i), newUser.lastName);
  await userEvent.type(screen.getByLabelText(/email address/i), newUser.email);
  await userEvent.type(screen.getByLabelText(/password/i), newUser.password);
  await userEvent.type(screen.getByLabelText(/team name/i), newUser.teamName);

  await userEvent.click(screen.getByRole('button', { name: /register/i }));

  await waitFor(() => expect(onSuccess).toHaveBeenCalledTimes(1));
});


================================================
FILE: apps/nextjs-app/src/features/auth/components/login-form.tsx
================================================
'use client';

import NextLink from 'next/link';
import { useSearchParams } from 'next/navigation';

import { Button } from '@/components/ui/button';
import { Form, Input } from '@/components/ui/form';
import { paths } from '@/config/paths';
import { useLogin, loginInputSchema } from '@/lib/auth';

type LoginFormProps = {
  onSuccess: () => void;
};

export const LoginForm = ({ onSuccess }: LoginFormProps) => {
  const login = useLogin({
    onSuccess,
  });

  const searchParams = useSearchParams();
  const redirectTo = searchParams?.get('redirectTo');
  return (
    <div>
      <Form
        onSubmit={(values) => {
          login.mutate(values);
        }}
        schema={loginInputSchema}
      >
        {({ register, formState }) => (
          <>
            <Input
              type="email"
              label="Email Address"
              error={formState.errors['email']}
              registration={register('email')}
            />
            <Input
              type="password"
              label="Password"
              error={formState.errors['password']}
              registration={register('password')}
            />
            <div>
              <Button
                isLoading={login.isPending}
                type="submit"
                className="w-full"
              >
                Log in
              </Button>
            </div>
          </>
        )}
      </Form>
      <div className="mt-2 flex items-center justify-end">
        <div className="text-sm">
          <NextLink
            href={paths.auth.register.getHref(redirectTo)}
            className="font-medium text-blue-600 hover:text-blue-500"
          >
            Register
          </NextLink>
        </div>
      </div>
    </div>
  );
};


================================================
FILE: apps/nextjs-app/src/features/auth/components/register-form.tsx
================================================
'use client';

import NextLink from 'next/link';
import { useSearchParams } from 'next/navigation';
import * as React from 'react';

import { Button } from '@/components/ui/button';
import { Form, Input, Select, Label, Switch } from '@/components/ui/form';
import { paths } from '@/config/paths';
import { useRegister, registerInputSchema } from '@/lib/auth';
import { Team } from '@/types/api';

type RegisterFormProps = {
  onSuccess: () => void;
  chooseTeam: boolean;
  setChooseTeam: () => void;
  teams?: Team[];
};

export const RegisterForm = ({
  onSuccess,
  chooseTeam,
  setChooseTeam,
  teams,
}: RegisterFormProps) => {
  const registering = useRegister({ onSuccess });
  const searchParams = useSearchParams();
  const redirectTo = searchParams?.get('redirectTo');

  return (
    <div>
      <Form
        onSubmit={(values) => {
          registering.mutate(values);
        }}
        schema={registerInputSchema}
        options={{
          shouldUnregister: true,
        }}
      >
        {({ register, formState }) => (
          <>
            <Input
              type="text"
              label="First Name"
              error={formState.errors['firstName']}
              registration={register('firstName')}
            />
            <Input
              type="text"
              label="Last Name"
              error={formState.errors['lastName']}
              registration={register('lastName')}
            />
            <Input
              type="email"
              label="Email Address"
              error={formState.errors['email']}
              registration={register('email')}
            />
            <Input
              type="password"
              label="Password"
              error={formState.errors['password']}
              registration={register('password')}
            />

            <div className="flex items-center space-x-2">
              <Switch
                checked={chooseTeam}
                onCheckedChange={setChooseTeam}
                className={`${
                  chooseTeam ? 'bg-blue-600' : 'bg-gray-200'
                } relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-2`}
                id="choose-team"
              />
              <Label htmlFor="airplane-mode">Join Existing Team</Label>
            </div>

            {chooseTeam && teams ? (
              <Select
                label="Team"
                error={formState.errors['teamId']}
                registration={register('teamId')}
                options={teams?.map((team) => ({
                  label: team.name,
                  value: team.id,
                }))}
              />
            ) : (
              <Input
                type="text"
                label="Team Name"
                error={formState.errors['teamName']}
                registration={register('teamName')}
              />
            )}
            <div>
              <Button
                isLoading={registering.isPending}
                type="submit"
                className="w-full"
              >
                Register
              </Button>
            </div>
          </>
        )}
      </Form>
      <div className="mt-2 flex items-center justify-end">
        <div className="text-sm">
          <NextLink
            href={paths.auth.login.getHref(redirectTo)}
            className="font-medium text-blue-600 hover:text-blue-500"
          >
            Log In
          </NextLink>
        </div>
      </div>
    </div>
  );
};


================================================
FILE: apps/nextjs-app/src/features/comments/api/create-comment.ts
================================================
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { z } from 'zod';

import { api } from '@/lib/api-client';
import { MutationConfig } from '@/lib/react-query';
import { Comment } from '@/types/api';

import { getInfiniteCommentsQueryOptions } from './get-comments';

export const createCommentInputSchema = z.object({
  discussionId: z.string().min(1, 'Required'),
  body: z.string().min(1, 'Required'),
});

export type CreateCommentInput = z.infer<typeof createCommentInputSchema>;

export const createComment = ({
  data,
}: {
  data: CreateCommentInput;
}): Promise<Comment> => {
  return api.post('/comments', data);
};

type UseCreateCommentOptions = {
  discussionId: string;
  mutationConfig?: MutationConfig<typeof createComment>;
};

export const useCreateComment = ({
  mutationConfig,
  discussionId,
}: UseCreateCommentOptions) => {
  const queryClient = useQueryClient();

  const { onSuccess, ...restConfig } = mutationConfig || {};

  return useMutation({
    onSuccess: (...args) => {
      queryClient.invalidateQueries({
        queryKey: getInfiniteCommentsQueryOptions(discussionId).queryKey,
      });
      onSuccess?.(...args);
    },
    ...restConfig,
    mutationFn: createComment,
  });
};


================================================
FILE: apps/nextjs-app/src/features/comments/api/delete-comment.ts
================================================
import { useMutation, useQueryClient } from '@tanstack/react-query';

import { api } from '@/lib/api-client';
import { MutationConfig } from '@/lib/react-query';

import { getInfiniteCommentsQueryOptions } from './get-comments';

export const deleteComment = ({ commentId }: { commentId: string }) => {
  return api.delete(`/comments/${commentId}`);
};

type UseDeleteCommentOptions = {
  discussionId: string;
  mutationConfig?: MutationConfig<typeof deleteComment>;
};

export const useDeleteComment = ({
  mutationConfig,
  discussionId,
}: UseDeleteCommentOptions) => {
  const queryClient = useQueryClient();

  const { onSuccess, ...restConfig } = mutationConfig || {};

  return useMutation({
    onSuccess: (...args) => {
      queryClient.invalidateQueries({
        queryKey: getInfiniteCommentsQueryOptions(discussionId).queryKey,
      });
      onSuccess?.(...args);
    },
    ...restConfig,
    mutationFn: deleteComment,
  });
};


================================================
FILE: apps/nextjs-app/src/features/comments/api/get-comments.ts
================================================
import { infiniteQueryOptions, useInfiniteQuery } from '@tanstack/react-query';

import { api } from '@/lib/api-client';
import { QueryConfig } from '@/lib/react-query';
import { Comment, Meta } from '@/types/api';

export const getComments = ({
  discussionId,
  page = 1,
}: {
  discussionId: string;
  page?: number;
}): Promise<{ data: Comment[]; meta: Meta }> => {
  return api.get(`/comments`, {
    params: {
      discussionId,
      page,
    },
  });
};

export const getInfiniteCommentsQueryOptions = (discussionId: string) => {
  return infiniteQueryOptions({
    queryKey: ['comments', discussionId],
    queryFn: ({ pageParam = 1 }) => {
      return getComments({ discussionId, page: pageParam as number });
    },
    getNextPageParam: (lastPage) => {
      if (lastPage?.meta?.page === lastPage?.meta?.totalPages) return undefined;
      const nextPage = lastPage.meta.page + 1;
      return nextPage;
    },
    initialPageParam: 1,
  });
};

type UseCommentsOptions = {
  discussionId: string;
  page?: number;
  queryConfig?: QueryConfig<typeof getComments>;
};

export const useInfiniteComments = ({ discussionId }: UseCommentsOptions) => {
  return useInfiniteQuery({
    ...getInfiniteCommentsQueryOptions(discussionId),
  });
};


================================================
FILE: apps/nextjs-app/src/features/comments/components/comments-list.tsx
================================================
'use client';

import { ArchiveX } from 'lucide-react';
import { usePathname } from 'next/navigation';

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 { canDeleteComment } from '@/lib/authorization';
import { formatDate } from '@/utils/format';

import { useInfiniteComments } from '../api/get-comments';

import { DeleteComment } from './delete-comment';

type CommentsListProps = {
  discussionId: string;
};

export const CommentsList = ({ discussionId }: CommentsListProps) => {
  const user = useUser();
  const commentsQuery = useInfiniteComments({ discussionId });
  const pathname = usePathname();
  const isPublicView = pathname?.startsWith?.('/public/');

  if (commentsQuery.isLoading) {
    return (
      <div className="flex h-48 w-full items-center justify-center">
        <Spinner size="lg" />
      </div>
    );
  }

  const comments = commentsQuery.data?.pages.flatMap((page) => page.data);

  if (!comments?.length)
    return (
      <div
        role="list"
        aria-label="comments"
        className="flex h-40 flex-col items-center justify-center bg-white text-gray-500"
      >
        <ArchiveX className="size-10" />
        <h4>No Comments Found</h4>
      </div>
    );

  return (
    <>
      <ul aria-label="comments" className="flex flex-col space-y-3">
        {comments.map((comment, index) => (
          <li
            aria-label={`comment-${comment.body}-${index}`}
            key={comment.id || index}
            className="w-full bg-white p-4 shadow-sm"
          >
            <div className="flex justify-between">
              <div>
                <span className="text-xs font-semibold">
                  {formatDate(comment.createdAt)}
                </span>
                {comment.author && (
                  <span className="text-xs font-bold">
                    {' '}
                    by {comment.author.firstName} {comment.author.lastName}
                  </span>
                )}
              </div>
              {!isPublicView && canDeleteComment(user.data, comment) && (
                <DeleteComment discussionId={discussionId} id={comment.id} />
              )}
            </div>
            <MDPreview value={comment.body} />
          </li>
        ))}
      </ul>
      {commentsQuery.hasNextPage && (
        <div className="flex items-center justify-center py-4">
          <Button onClick={() => commentsQuery.fetchNextPage()}>
            {commentsQuery.isFetchingNextPage ? (
              <Spinner />
            ) : (
              'Load More Comments'
            )}
          </Button>
        </div>
      )}
    </>
  );
};


================================================
FILE: apps/nextjs-app/src/features/comments/components/comments.tsx
================================================
'use client';

import { usePathname } from 'next/navigation';

import { CommentsList } from './comments-list';
import { CreateComment } from './create-comment';

type CommentsProps = {
  discussionId: string;
};

export const Comments = ({ discussionId }: CommentsProps) => {
  const pathname = usePathname();
  const isPublicView = pathname?.startsWith?.('/public/');
  return (
    <div>
      <div className="mb-4 flex items-center justify-between">
        <h3 className="text-xl font-bold">Comments:</h3>
        {!isPublicView && <CreateComment discussionId={discussionId} />}
      </div>
      <CommentsList discussionId={discussionId} />
    </div>
  );
};


================================================
FILE: apps/nextjs-app/src/features/comments/components/create-comment.tsx
================================================
'use client';

import { Plus } from 'lucide-react';

import { Button } from '@/components/ui/button';
import { Form, FormDrawer, Textarea } from '@/components/ui/form';
import { useNotifications } from '@/components/ui/notifications';

import {
  useCreateComment,
  createCommentInputSchema,
} from '../api/create-comment';

type CreateCommentProps = {
  discussionId: string;
};

export const CreateComment = ({ discussionId }: CreateCommentProps) => {
  const { addNotification } = useNotifications();
  const createCommentMutation = useCreateComment({
    discussionId,
    mutationConfig: {
      onSuccess: () => {
        addNotification({
          type: 'success',
          title: 'Comment Created',
        });
      },
    },
  });

  return (
    <FormDrawer
      isDone={createCommentMutation.isSuccess}
      triggerButton={
        <Button size="sm" icon={<Plus className="size-4" />}>
          Create Comment
        </Button>
      }
      title="Create Comment"
      submitButton={
        <Button
          isLoading={createCommentMutation.isPending}
          form="create-comment"
          type="submit"
          size="sm"
          disabled={createCommentMutation.isPending}
        >
          Submit
        </Button>
      }
    >
      <Form
        id="create-comment"
        onSubmit={(values) => {
          createCommentMutation.mutate({
            data: values,
          });
        }}
        schema={createCommentInputSchema}
        options={{
          defaultValues: {
            body: '',
            discussionId: discussionId,
          },
        }}
      >
        {({ register, formState }) => (
          <Textarea
            label="Body"
            error={formState.errors['body']}
            registration={register('body')}
          />
        )}
      </Form>
    </FormDrawer>
  );
};


================================================
FILE: apps/nextjs-app/src/features/comments/components/delete-comment.tsx
================================================
'use client';

import { Trash } from 'lucide-react';

import { Button } from '@/components/ui/button';
import { ConfirmationDialog } from '@/components/ui/dialog';
import { useNotifications } from '@/components/ui/notifications';

import { useDeleteComment } from '../api/delete-comment';

type DeleteCommentProps = {
  id: string;
  discussionId: string;
};

export const DeleteComment = ({ id, discussionId }: DeleteCommentProps) => {
  const { addNotification } = useNotifications();
  const deleteCommentMutation = useDeleteComment({
    discussionId,
    mutationConfig: {
      onSuccess: () => {
        addNotification({
          type: 'success',
          title: 'Comment Deleted',
        });
      },
    },
  });

  return (
    <ConfirmationDialog
      isDone={deleteCommentMutation.isSuccess}
      icon="danger"
      title="Delete Comment"
      body="Are you sure you want to delete this comment?"
      triggerButton={
        <Button
          variant="destructive"
          size="sm"
          icon={<Trash className="size-4" />}
        >
          Delete Comment
        </Button>
      }
      confirmButton={
        <Button
          isLoading={deleteCommentMutation.isPending}
          type="button"
          variant="destructive"
          onClick={() => deleteCommentMutation.mutate({ commentId: id })}
        >
          Delete Comment
        </Button>
      }
    />
  );
};


================================================
FILE: apps/nextjs-app/src/features/discussions/api/create-discussion.ts
================================================
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { z } from 'zod';

import { api } from '@/lib/api-client';
import { MutationConfig } from '@/lib/react-query';
import { Discussion } from '@/types/api';

import { getDiscussionsQueryOptions } from './get-discussions';

export const createDiscussionInputSchema = z.object({
  title: z.string().min(1, 'Required'),
  body: z.string().min(1, 'Required'),
  public: z.boolean(),
});

export type CreateDiscussionInput = z.infer<typeof createDiscussionInputSchema>;

export const createDiscussion = ({
  data,
}: {
  data: CreateDiscussionInput;
}): Promise<Discussion> => {
  return api.post(`/discussions`, data);
};

type UseCreateDiscussionOptions = {
  mutationConfig?: MutationConfig<typeof createDiscussion>;
};

export const useCreateDiscussion = ({
  mutationConfig,
}: UseCreateDiscussionOptions = {}) => {
  const queryClient = useQueryClient();

  const { onSuccess, ...restConfig } = mutationConfig || {};

  return useMutation({
    onSuccess: (...args) => {
      queryClient.invalidateQueries({
        queryKey: getDiscussionsQueryOptions().queryKey,
      });
      onSuccess?.(...args);
    },
    ...restConfig,
    mutationFn: createDiscussion,
  });
};


================================================
FILE: apps/nextjs-app/src/features/discussions/api/delete-discussion.ts
================================================
import { useMutation, useQueryClient } from '@tanstack/react-query';

import { api } from '@/lib/api-client';
import { MutationConfig } from '@/lib/react-query';

import { getDiscussionsQueryOptions } from './get-discussions';

export const deleteDiscussion = ({
  discussionId,
}: {
  discussionId: string;
}) => {
  return api.delete(`/discussions/${discussionId}`);
};

type UseDeleteDiscussionOptions = {
  mutationConfig?: MutationConfig<typeof deleteDiscussion>;
};

export const useDeleteDiscussion = ({
  mutationConfig,
}: UseDeleteDiscussionOptions = {}) => {
  const queryClient = useQueryClient();

  const { onSuccess, ...restConfig } = mutationConfig || {};

  return useMutation({
    onSuccess: (...args) => {
      queryClient.invalidateQueries({
        queryKey: getDiscussionsQueryOptions().queryKey,
      });
      onSuccess?.(...args);
    },
    ...restConfig,
    mutationFn: deleteDiscussion,
  });
};


================================================
FILE: apps/nextjs-app/src/features/discussions/api/get-discussion.ts
================================================
import { useQuery, queryOptions } from '@tanstack/react-query';

import { api } from '@/lib/api-client';
import { QueryConfig } from '@/lib/react-query';
import { Discussion } from '@/types/api';

export const getDiscussion = ({
  discussionId,
}: {
  discussionId: string;
}): Promise<{ data: Discussion }> => {
  return api.get(`/discussions/${discussionId}`);
};

export const getDiscussionQueryOptions = (discussionId: string) => {
  return queryOptions({
    queryKey: ['discussions', discussionId],
    queryFn: () => getDiscussion({ discussionId }),
  });
};

type UseDiscussionOptions = {
  discussionId: string;
  queryConfig?: QueryConfig<typeof getDiscussionQueryOptions>;
};

export const useDiscussion = ({
  discussionId,
  queryConfig,
}: UseDiscussionOptions) => {
  return useQuery({
    ...getDiscussionQueryOptions(discussionId),
    ...queryConfig,
  });
};


================================================
FILE: apps/nextjs-app/src/features/discussions/api/get-discussions.ts
================================================
import { queryOptions, useQuery } from '@tanstack/react-query';

import { api } from '@/lib/api-client';
import { QueryConfig } from '@/lib/react-query';
import { Discussion, Meta } from '@/types/api';

export const getDiscussions = (
  { page }: { page?: number } = { page: 1 },
): Promise<{
  data: Discussion[];
  meta: Meta;
}> => {
  return api.get(`/discussions`, {
    params: {
      page,
    },
  });
};

export const getDiscussionsQueryOptions = ({
  page = 1,
}: { page?: number } = {}) => {
  return queryOptions({
    queryKey: ['discussions', { page }],
    queryFn: () => getDiscussions({ page }),
  });
};

type UseDiscussionsOptions = {
  page?: number;
  queryConfig?: QueryConfig<typeof getDiscussionsQueryOptions>;
};

export const useDiscussions = ({
  queryConfig,
  page,
}: UseDiscussionsOptions) => {
  return useQuery({
    ...getDiscussionsQueryOptions({ page }),
    ...queryConfig,
  });
};


================================================
FILE: apps/nextjs-app/src/features/discussions/api/update-discussion.ts
================================================
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { z } from 'zod';

import { api } from '@/lib/api-client';
import { MutationConfig } from '@/lib/react-query';
import { Discussion } from '@/types/api';

import { getDiscussionQueryOptions } from './get-discussion';

export const updateDiscussionInputSchema = z.object({
  title: z.string().min(1, 'Required'),
  body: z.string().min(1, 'Required'),
  public: z.boolean(),
});

export type UpdateDiscussionInput = z.infer<typeof updateDiscussionInputSchema>;

export const updateDiscussion = ({
  data,
  discussionId,
}: {
  data: UpdateDiscussionInput;
  discussionId: string;
}): Promise<Discussion> => {
  return api.patch(`/discussions/${discussionId}`, data);
};

type UseUpdateDiscussionOptions = {
  mutationConfig?: MutationConfig<typeof updateDiscussion>;
};

export const useUpdateDiscussion = ({
  mutationConfig,
}: UseUpdateDiscussionOptions = {}) => {
  const queryClient = useQueryClient();

  const { onSuccess, ...restConfig } = mutationConfig || {};

  return useMutation({
    onSuccess: (data, ...args) => {
      queryClient.refetchQueries({
        queryKey: getDiscussionQueryOptions(data.id).queryKey,
      });
      onSuccess?.(data, ...args);
    },
    ...restConfig,
    mutationFn: updateDiscussion,
  });
};


================================================
FILE: apps/nextjs-app/src/features/discussions/components/create-discussion.tsx
================================================
'use client';

import { Plus } from 'lucide-react';

import { Button } from '@/components/ui/button';
import {
  Form,
  FormDrawer,
  Input,
  Label,
  Switch,
  Textarea,
} from '@/components/ui/form';
import { useNotifications } from '@/components/ui/notifications';
import { useUser } from '@/lib/auth';
import { canCreateDiscussion } from '@/lib/authorization';

import {
  createDiscussionInputSchema,
  useCreateDiscussion,
} from '../api/create-discussion';

export const CreateDiscussion = () => {
  const { addNotification } = useNotifications();
  const createDiscussionMutation = useCreateDiscussion({
    mutationConfig: {
      onSuccess: () => {
        addNotification({
          type: 'success',
          title: 'Discussion Created',
        });
      },
    },
  });

  const user = useUser();

  if (!canCreateDiscussion(user?.data)) {
    return null;
  }

  return (
    <FormDrawer
      isDone={createDiscussionMutation.isSuccess}
      triggerButton={
        <Button size="sm" icon={<Plus className="size-4" />}>
          Create Discussion
        </Button>
      }
      title="Create Discussion"
      submitButton={
        <Button
          form="create-discussion"
          type="submit"
          size="sm"
          isLoading={createDiscussionMutation.isPending}
        >
          Submit
        </Button>
      }
    >
      <Form
        id="create-discussion"
        onSubmit={(values) => {
          createDiscussionMutation.mutate({ data: values });
        }}
        schema={createDiscussionInputSchema}
        options={{
          defaultValues: {
            title: '',
            body: '',
            public: false,
          },
        }}
      >
        {({ register, formState, setValue, watch }) => (
          <>
            <Input
              label="Title"
              error={formState.errors['title']}
              registration={register('title')}
            />

            <Textarea
              label="Body"
              error={formState.errors['body']}
              registration={register('body')}
            />

            <div className="flex items-center space-x-2">
              <Switch
                name="public"
                onCheckedChange={(value) => setValue('public', value)}
                checked={watch('public')}
                className={` relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-2`}
                id="public"
              />
              <Label htmlFor="airplane-mode">Public</Label>
            </div>
          </>
        )}
      </Form>
    </FormDrawer>
  );
};


================================================
FILE: apps/nextjs-app/src/features/discussions/components/delete-discussion.tsx
================================================
'use client';

import { Trash } from 'lucide-react';

import { Button } from '@/components/ui/button';
import { ConfirmationDialog } from '@/components/ui/dialog';
import { useNotifications } from '@/components/ui/notifications';
import { useUser } from '@/lib/auth';
import { canDeleteDiscussion } from '@/lib/authorization';

import { useDeleteDiscussion } from '../api/delete-discussion';

type DeleteDiscussionProps = {
  id: string;
};

export const DeleteDiscussion = ({ id }: DeleteDiscussionProps) => {
  const user = useUser();
  const { addNotification } = useNotifications();
  const deleteDiscussionMutation = useDeleteDiscussion({
    mutationConfig: {
      onSuccess: () => {
        addNotification({
          type: 'success',
          title: 'Discussion Deleted',
        });
      },
    },
  });

  if (!canDeleteDiscussion(user?.data)) {
    return null;
  }

  return (
    <ConfirmationDialog
      icon="danger"
      title="Delete Discussion"
      body="Are you sure you want to delete this discussion?"
      triggerButton={
        <Button variant="destructive" icon={<Trash className="size-4" />}>
          Delete Discussion
        </Button>
      }
      confirmButton={
        <Button
          isLoading={deleteDiscussionMutation.isPending}
          type="button"
          variant="destructive"
          onClick={() => deleteDiscussionMutation.mutate({ discussionId: id })}
        >
          Delete Discussion
        </Button>
      }
    />
  );
};


================================================
FILE: apps/nextjs-app/src/features/discussions/components/discussion-view.tsx
================================================
'use client';

import { Link as LinkIcon } from 'lucide-react';
import { usePathname } from 'next/navigation';

import { Link } from '@/components/ui/link';
import { MDPreview } from '@/components/ui/md-preview';
import { Spinner } from '@/components/ui/spinner';
import { paths } from '@/config/paths';
import { formatDate } from '@/utils/format';

import { useDiscussion } from '../api/get-discussion';
import { UpdateDiscussion } from '../components/update-discussion';

export const DiscussionView = ({ discussionId }: { discussionId: string }) => {
  const pathname = usePathname();
  const isPublicView = pathname?.startsWith?.('/public/');

  const discussionQuery = useDiscussion({
    discussionId,
  });

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

  const discussion = discussionQuery?.data?.data;

  if (!discussion) return null;

  return (
    <div>
      <div className="flex justify-between">
        <span>
          <span className="text-xs font-bold">
            {formatDate(discussion.createdAt)}
          </span>
          {discussion.author && (
            <span className="ml-2 text-sm font-bold">
              by {discussion.author.firstName} {discussion.author.lastName}
            </span>
          )}
        </span>
        {!isPublicView && discussion.public && (
          <Link
            className="ml-2 flex items-center gap-2 text-sm font-bold"
            href={paths.public.discussion.getHref(discussionId)}
            target="_blank"
          >
            View Public Version <LinkIcon size={16} />
          </Link>
        )}
      </div>
      <div className="mt-6 flex flex-col space-y-16">
        {!isPublicView && (
          <div className="flex justify-end">
            <UpdateDiscussion discussionId={discussionId} />
          </div>
        )}
        <div>
          <div className="overflow-hidden bg-white shadow sm:rounded-lg">
            <div className="px-4 py-5 sm:px-6">
              <div className="mt-1 max-w-2xl text-sm text-gray-500">
                <MDPreview value={discussion.body} />
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
};


================================================
FILE: apps/nextjs-app/src/features/discussions/components/discussions-list.tsx
================================================
'use client';

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

import { Link } from '@/components/ui/link';
import { Spinner } from '@/components/ui/spinner';
import { Table } from '@/components/ui/table';
import { paths } from '@/config/paths';
import { formatDate } from '@/utils/format';

import { getDiscussionQueryOptions } from '../api/get-discussion';
import { useDiscussions } from '../api/get-discussions';

import { DeleteDiscussion } from './delete-discussion';

export type DiscussionsListProps = {
  onDiscussionPrefetch?: (id: string) => void;
};

export const DiscussionsList = ({
  onDiscussionPrefetch,
}: DiscussionsListProps) => {
  const searchParams = useSearchParams();
  const page = searchParams?.get('page') ? Number(searchParams.get('page')) : 1;

  const discussionsQuery = useDiscussions({
    page: page,
  });
  const queryClient = useQueryClient();

  if (discussionsQuery.isLoading) {
    return (
      <div className="flex h-48 w-full items-center justify-center">
        <Spinner size="lg" />
      </div>
    );
  }

  const discussions = discussionsQuery.data?.data;
  const meta = discussionsQuery.data?.meta;

  if (!discussions) return null;

  return (
    <Table
      data={discussions}
      columns={[
        {
          title: 'Title',
          field: 'title',
        },
        {
          title: 'Created At',
          field: 'createdAt',
          Cell({ entry: { createdAt } }) {
            return <span>{formatDate(createdAt)}</span>;
          },
        },
        {
          title: '',
          field: 'id',
          Cell({ entry: { id } }) {
            return (
              <Link
                onMouseEnter={() => {
                  // Prefetch the discussion data when the user hovers over the link
                  queryClient.prefetchQuery(getDiscussionQueryOptions(id));
                  onDiscussionPrefetch?.(id);
                }}
                href={paths.app.discussion.getHref(id)}
              >
                View
              </Link>
            );
          },
        },
        {
          title: '',
          field: 'id',
          Cell({ entry: { id } }) {
            return <DeleteDiscussion id={id} />;
          },
        },
      ]}
      pagination={
        meta && {
          totalPages: meta.totalPages,
          currentPage: meta.page,
          rootUrl: '',
        }
      }
    />
  );
};


================================================
FILE: apps/nextjs-app/src/features/discussions/components/update-discussion.tsx
================================================
'use client';

import { Pen } from 'lucide-react';

import { Button } from '@/components/ui/button';
import {
  Form,
  FormDrawer,
  Input,
  Label,
  Switch,
  Textarea,
} from '@/components/ui/form';
import { useNotifications } from '@/components/ui/notifications';
import { useUser } from '@/lib/auth';
import { canUpdateDiscussion } from '@/lib/authorization';

import { useDiscussion } from '../api/get-discussion';
import {
  updateDiscussionInputSchema,
  useUpdateDiscussion,
} from '../api/update-discussion';

type UpdateDiscussionProps = {
  discussionId: string;
};

export const UpdateDiscussion = ({ discussionId }: UpdateDiscussionProps) => {
  const { addNotification } = useNotifications();
  const discussionQuery = useDiscussion({ discussionId });
  const updateDiscussionMutation = useUpdateDiscussion({
    mutationConfig: {
      onSuccess: () => {
        addNotification({
          type: 'success',
          title: 'Discussion Updated',
        });
      },
    },
  });

  const user = useUser();

  if (!canUpdateDiscussion(user?.data)) {
    return null;
  }

  const discussion = discussionQuery.data?.data;

  return (
    <FormDrawer
      isDone={updateDiscussionMutation.isSuccess}
      triggerButton={
        <Button icon={<Pen className="size-4" />} size="sm">
          Update Discussion
  
Download .txt
gitextract_dmcys4tl/

├── .github/
│   └── workflows/
│       ├── nextjs-app-ci.yml
│       ├── nextjs-pages-ci.yml
│       └── react-vite-ci.yml
├── .gitignore
├── .husky/
│   └── pre-commit
├── LICENSE
├── README.md
├── apps/
│   ├── nextjs-app/
│   │   ├── .eslintrc.cjs
│   │   ├── .gitignore
│   │   ├── .prettierignore
│   │   ├── .prettierrc
│   │   ├── .storybook/
│   │   │   ├── main.ts
│   │   │   └── preview.tsx
│   │   ├── .vscode/
│   │   │   ├── extensions.json
│   │   │   └── settings.json
│   │   ├── README.md
│   │   ├── __mocks__/
│   │   │   ├── vitest-env.d.ts
│   │   │   └── zustand.ts
│   │   ├── e2e/
│   │   │   ├── .eslintrc.cjs
│   │   │   └── tests/
│   │   │       ├── auth.setup.ts
│   │   │       ├── profile.spec.ts
│   │   │       └── smoke.spec.ts
│   │   ├── generators/
│   │   │   └── component/
│   │   │       ├── component.stories.tsx.hbs
│   │   │       ├── component.tsx.hbs
│   │   │       ├── index.cjs
│   │   │       └── index.ts.hbs
│   │   ├── index.html
│   │   ├── lint-staged.config.mjs
│   │   ├── mock-server.ts
│   │   ├── next-env.d.ts
│   │   ├── next.config.mjs
│   │   ├── package.json
│   │   ├── playwright.config.ts
│   │   ├── plopfile.cjs
│   │   ├── postcss.config.cjs
│   │   ├── public/
│   │   │   ├── _redirects
│   │   │   ├── mockServiceWorker.js
│   │   │   └── robots.txt
│   │   ├── src/
│   │   │   ├── app/
│   │   │   │   ├── app/
│   │   │   │   │   ├── _components/
│   │   │   │   │   │   ├── dashboard-info.tsx
│   │   │   │   │   │   └── dashboard-layout.tsx
│   │   │   │   │   ├── discussions/
│   │   │   │   │   │   ├── [discussionId]/
│   │   │   │   │   │   │   ├── __tests__/
│   │   │   │   │   │   │   │   └── discussion.test.tsx
│   │   │   │   │   │   │   ├── _components/
│   │   │   │   │   │   │   │   └── discussion.tsx
│   │   │   │   │   │   │   └── page.tsx
│   │   │   │   │   │   ├── __tests__/
│   │   │   │   │   │   │   └── discussions.test.tsx
│   │   │   │   │   │   ├── _components/
│   │   │   │   │   │   │   └── discussions.tsx
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   ├── layout.tsx
│   │   │   │   │   ├── page.tsx
│   │   │   │   │   ├── profile/
│   │   │   │   │   │   ├── _components/
│   │   │   │   │   │   │   └── profile.tsx
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   └── users/
│   │   │   │   │       ├── _components/
│   │   │   │   │       │   ├── admin-guard.tsx
│   │   │   │   │       │   └── users.tsx
│   │   │   │   │       └── page.tsx
│   │   │   │   ├── auth/
│   │   │   │   │   ├── _components/
│   │   │   │   │   │   └── auth-layout.tsx
│   │   │   │   │   ├── layout.tsx
│   │   │   │   │   ├── login/
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   └── register/
│   │   │   │   │       └── page.tsx
│   │   │   │   ├── layout.tsx
│   │   │   │   ├── not-found.tsx
│   │   │   │   ├── page.tsx
│   │   │   │   ├── provider.tsx
│   │   │   │   └── public/
│   │   │   │       └── discussions/
│   │   │   │           └── [discussionId]/
│   │   │   │               └── page.tsx
│   │   │   ├── components/
│   │   │   │   ├── errors/
│   │   │   │   │   └── main.tsx
│   │   │   │   ├── layouts/
│   │   │   │   │   └── content-layout.tsx
│   │   │   │   └── ui/
│   │   │   │       ├── button/
│   │   │   │       │   ├── button.stories.tsx
│   │   │   │       │   ├── button.tsx
│   │   │   │       │   └── index.ts
│   │   │   │       ├── dialog/
│   │   │   │       │   ├── __tests__/
│   │   │   │       │   │   └── dialog.test.tsx
│   │   │   │       │   ├── confirmation-dialog/
│   │   │   │       │   │   ├── __tests__/
│   │   │   │       │   │   │   └── confirmation-dialog.test.tsx
│   │   │   │       │   │   ├── confirmation-dialog.stories.tsx
│   │   │   │       │   │   ├── confirmation-dialog.tsx
│   │   │   │       │   │   └── index.ts
│   │   │   │       │   ├── dialog.stories.tsx
│   │   │   │       │   ├── dialog.tsx
│   │   │   │       │   └── index.ts
│   │   │   │       ├── drawer/
│   │   │   │       │   ├── __tests__/
│   │   │   │       │   │   └── drawer.test.tsx
│   │   │   │       │   ├── drawer.stories.tsx
│   │   │   │       │   ├── drawer.tsx
│   │   │   │       │   └── index.ts
│   │   │   │       ├── dropdown/
│   │   │   │       │   ├── dropdown.stories.tsx
│   │   │   │       │   ├── dropdown.tsx
│   │   │   │       │   └── index.ts
│   │   │   │       ├── form/
│   │   │   │       │   ├── __tests__/
│   │   │   │       │   │   └── form.test.tsx
│   │   │   │       │   ├── error.tsx
│   │   │   │       │   ├── field-wrapper.tsx
│   │   │   │       │   ├── form-drawer.tsx
│   │   │   │       │   ├── form.stories.tsx
│   │   │   │       │   ├── form.tsx
│   │   │   │       │   ├── index.ts
│   │   │   │       │   ├── input.tsx
│   │   │   │       │   ├── label.tsx
│   │   │   │       │   ├── select.tsx
│   │   │   │       │   ├── switch.tsx
│   │   │   │       │   └── textarea.tsx
│   │   │   │       ├── link/
│   │   │   │       │   ├── index.ts
│   │   │   │       │   ├── link.stories.tsx
│   │   │   │       │   └── link.tsx
│   │   │   │       ├── md-preview/
│   │   │   │       │   ├── index.ts
│   │   │   │       │   ├── md-preview.stories.tsx
│   │   │   │       │   └── md-preview.tsx
│   │   │   │       ├── notifications/
│   │   │   │       │   ├── __tests__/
│   │   │   │       │   │   └── notifications.test.ts
│   │   │   │       │   ├── index.ts
│   │   │   │       │   ├── notification.stories.tsx
│   │   │   │       │   ├── notification.tsx
│   │   │   │       │   ├── notifications-store.ts
│   │   │   │       │   └── notifications.tsx
│   │   │   │       ├── spinner/
│   │   │   │       │   ├── index.ts
│   │   │   │       │   ├── spinner.stories.tsx
│   │   │   │       │   └── spinner.tsx
│   │   │   │       └── table/
│   │   │   │           ├── index.ts
│   │   │   │           ├── pagination.tsx
│   │   │   │           ├── table.stories.tsx
│   │   │   │           └── table.tsx
│   │   │   ├── config/
│   │   │   │   ├── env.ts
│   │   │   │   └── paths.ts
│   │   │   ├── features/
│   │   │   │   ├── auth/
│   │   │   │   │   └── components/
│   │   │   │   │       ├── __tests__/
│   │   │   │   │       │   ├── login-form.test.tsx
│   │   │   │   │       │   └── register-form.test.tsx
│   │   │   │   │       ├── login-form.tsx
│   │   │   │   │       └── register-form.tsx
│   │   │   │   ├── comments/
│   │   │   │   │   ├── api/
│   │   │   │   │   │   ├── create-comment.ts
│   │   │   │   │   │   ├── delete-comment.ts
│   │   │   │   │   │   └── get-comments.ts
│   │   │   │   │   └── components/
│   │   │   │   │       ├── comments-list.tsx
│   │   │   │   │       ├── comments.tsx
│   │   │   │   │       ├── create-comment.tsx
│   │   │   │   │       └── delete-comment.tsx
│   │   │   │   ├── discussions/
│   │   │   │   │   ├── api/
│   │   │   │   │   │   ├── create-discussion.ts
│   │   │   │   │   │   ├── delete-discussion.ts
│   │   │   │   │   │   ├── get-discussion.ts
│   │   │   │   │   │   ├── get-discussions.ts
│   │   │   │   │   │   └── update-discussion.ts
│   │   │   │   │   └── components/
│   │   │   │   │       ├── create-discussion.tsx
│   │   │   │   │       ├── delete-discussion.tsx
│   │   │   │   │       ├── discussion-view.tsx
│   │   │   │   │       ├── discussions-list.tsx
│   │   │   │   │       └── update-discussion.tsx
│   │   │   │   ├── teams/
│   │   │   │   │   └── api/
│   │   │   │   │       └── get-teams.ts
│   │   │   │   └── users/
│   │   │   │       ├── api/
│   │   │   │       │   ├── delete-user.ts
│   │   │   │       │   ├── get-users.ts
│   │   │   │       │   └── update-profile.ts
│   │   │   │       └── components/
│   │   │   │           ├── delete-user.tsx
│   │   │   │           ├── update-profile.tsx
│   │   │   │           └── users-list.tsx
│   │   │   ├── hooks/
│   │   │   │   ├── __tests__/
│   │   │   │   │   └── use-disclosure.test.ts
│   │   │   │   └── use-disclosure.ts
│   │   │   ├── lib/
│   │   │   │   ├── __tests__/
│   │   │   │   │   └── authorization.test.tsx
│   │   │   │   ├── api-client.ts
│   │   │   │   ├── auth.tsx
│   │   │   │   ├── authorization.ts
│   │   │   │   └── react-query.ts
│   │   │   ├── styles/
│   │   │   │   └── globals.css
│   │   │   ├── testing/
│   │   │   │   ├── data-generators.ts
│   │   │   │   ├── mocks/
│   │   │   │   │   ├── browser.ts
│   │   │   │   │   ├── db.ts
│   │   │   │   │   ├── handlers/
│   │   │   │   │   │   ├── auth.ts
│   │   │   │   │   │   ├── comments.ts
│   │   │   │   │   │   ├── discussions.ts
│   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   ├── teams.ts
│   │   │   │   │   │   └── users.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── server.ts
│   │   │   │   │   └── utils.ts
│   │   │   │   ├── setup-tests.ts
│   │   │   │   └── test-utils.tsx
│   │   │   ├── types/
│   │   │   │   └── api.ts
│   │   │   └── utils/
│   │   │       ├── auth.ts
│   │   │       ├── cn.ts
│   │   │       └── format.ts
│   │   ├── tailwind.config.cjs
│   │   ├── tsconfig.json
│   │   └── vitest.config.ts
│   ├── nextjs-pages/
│   │   ├── .eslintrc.cjs
│   │   ├── .gitignore
│   │   ├── .prettierignore
│   │   ├── .prettierrc
│   │   ├── .storybook/
│   │   │   ├── main.ts
│   │   │   └── preview.tsx
│   │   ├── .vscode/
│   │   │   ├── extensions.json
│   │   │   └── settings.json
│   │   ├── README.md
│   │   ├── __mocks__/
│   │   │   ├── vitest-env.d.ts
│   │   │   └── zustand.ts
│   │   ├── e2e/
│   │   │   ├── .eslintrc.cjs
│   │   │   └── tests/
│   │   │       ├── auth.setup.ts
│   │   │       ├── profile.spec.ts
│   │   │       └── smoke.spec.ts
│   │   ├── generators/
│   │   │   └── component/
│   │   │       ├── component.stories.tsx.hbs
│   │   │       ├── component.tsx.hbs
│   │   │       ├── index.cjs
│   │   │       └── index.ts.hbs
│   │   ├── lint-staged.config.mjs
│   │   ├── mock-server.ts
│   │   ├── next-env.d.ts
│   │   ├── next.config.mjs
│   │   ├── package.json
│   │   ├── playwright.config.ts
│   │   ├── plopfile.cjs
│   │   ├── postcss.config.cjs
│   │   ├── public/
│   │   │   ├── _redirects
│   │   │   ├── mockServiceWorker.js
│   │   │   └── robots.txt
│   │   ├── src/
│   │   │   ├── app/
│   │   │   │   ├── pages/
│   │   │   │   │   ├── app/
│   │   │   │   │   │   ├── dashboard.tsx
│   │   │   │   │   │   ├── discussions/
│   │   │   │   │   │   │   ├── __tests__/
│   │   │   │   │   │   │   │   ├── discussion.test.tsx
│   │   │   │   │   │   │   │   └── discussions.test.tsx
│   │   │   │   │   │   │   ├── discussion.tsx
│   │   │   │   │   │   │   └── discussions.tsx
│   │   │   │   │   │   ├── profile.tsx
│   │   │   │   │   │   └── users.tsx
│   │   │   │   │   └── auth/
│   │   │   │   │       ├── login.tsx
│   │   │   │   │       └── register.tsx
│   │   │   │   └── provider.tsx
│   │   │   ├── components/
│   │   │   │   ├── errors/
│   │   │   │   │   └── main.tsx
│   │   │   │   ├── layouts/
│   │   │   │   │   ├── auth-layout.tsx
│   │   │   │   │   ├── content-layout.tsx
│   │   │   │   │   ├── dashboard-layout.tsx
│   │   │   │   │   └── index.ts
│   │   │   │   ├── seo/
│   │   │   │   │   ├── head.tsx
│   │   │   │   │   └── index.ts
│   │   │   │   └── ui/
│   │   │   │       ├── button/
│   │   │   │       │   ├── button.stories.tsx
│   │   │   │       │   ├── button.tsx
│   │   │   │       │   └── index.ts
│   │   │   │       ├── dialog/
│   │   │   │       │   ├── __tests__/
│   │   │   │       │   │   └── dialog.test.tsx
│   │   │   │       │   ├── confirmation-dialog/
│   │   │   │       │   │   ├── __tests__/
│   │   │   │       │   │   │   └── confirmation-dialog.test.tsx
│   │   │   │       │   │   ├── confirmation-dialog.stories.tsx
│   │   │   │       │   │   ├── confirmation-dialog.tsx
│   │   │   │       │   │   └── index.ts
│   │   │   │       │   ├── dialog.stories.tsx
│   │   │   │       │   ├── dialog.tsx
│   │   │   │       │   └── index.ts
│   │   │   │       ├── drawer/
│   │   │   │       │   ├── __tests__/
│   │   │   │       │   │   └── drawer.test.tsx
│   │   │   │       │   ├── drawer.stories.tsx
│   │   │   │       │   ├── drawer.tsx
│   │   │   │       │   └── index.ts
│   │   │   │       ├── dropdown/
│   │   │   │       │   ├── dropdown.stories.tsx
│   │   │   │       │   ├── dropdown.tsx
│   │   │   │       │   └── index.ts
│   │   │   │       ├── form/
│   │   │   │       │   ├── __tests__/
│   │   │   │       │   │   └── form.test.tsx
│   │   │   │       │   ├── error.tsx
│   │   │   │       │   ├── field-wrapper.tsx
│   │   │   │       │   ├── form-drawer.tsx
│   │   │   │       │   ├── form.stories.tsx
│   │   │   │       │   ├── form.tsx
│   │   │   │       │   ├── index.ts
│   │   │   │       │   ├── input.tsx
│   │   │   │       │   ├── label.tsx
│   │   │   │       │   ├── select.tsx
│   │   │   │       │   ├── switch.tsx
│   │   │   │       │   └── textarea.tsx
│   │   │   │       ├── link/
│   │   │   │       │   ├── index.ts
│   │   │   │       │   ├── link.stories.tsx
│   │   │   │       │   └── link.tsx
│   │   │   │       ├── md-preview/
│   │   │   │       │   ├── index.ts
│   │   │   │       │   ├── md-preview.stories.tsx
│   │   │   │       │   └── md-preview.tsx
│   │   │   │       ├── notifications/
│   │   │   │       │   ├── __tests__/
│   │   │   │       │   │   └── notifications.test.ts
│   │   │   │       │   ├── index.ts
│   │   │   │       │   ├── notification.stories.tsx
│   │   │   │       │   ├── notification.tsx
│   │   │   │       │   ├── notifications-store.ts
│   │   │   │       │   └── notifications.tsx
│   │   │   │       ├── spinner/
│   │   │   │       │   ├── index.ts
│   │   │   │       │   ├── spinner.stories.tsx
│   │   │   │       │   └── spinner.tsx
│   │   │   │       └── table/
│   │   │   │           ├── index.ts
│   │   │   │           ├── pagination.tsx
│   │   │   │           ├── table.stories.tsx
│   │   │   │           └── table.tsx
│   │   │   ├── config/
│   │   │   │   ├── env.ts
│   │   │   │   └── paths.ts
│   │   │   ├── features/
│   │   │   │   ├── auth/
│   │   │   │   │   └── components/
│   │   │   │   │       ├── __tests__/
│   │   │   │   │       │   ├── login-form.test.tsx
│   │   │   │   │       │   └── register-form.test.tsx
│   │   │   │   │       ├── login-form.tsx
│   │   │   │   │       └── register-form.tsx
│   │   │   │   ├── comments/
│   │   │   │   │   ├── api/
│   │   │   │   │   │   ├── create-comment.ts
│   │   │   │   │   │   ├── delete-comment.ts
│   │   │   │   │   │   └── get-comments.ts
│   │   │   │   │   └── components/
│   │   │   │   │       ├── comments-list.tsx
│   │   │   │   │       ├── comments.tsx
│   │   │   │   │       ├── create-comment.tsx
│   │   │   │   │       └── delete-comment.tsx
│   │   │   │   ├── discussions/
│   │   │   │   │   ├── api/
│   │   │   │   │   │   ├── create-discussion.ts
│   │   │   │   │   │   ├── delete-discussion.ts
│   │   │   │   │   │   ├── get-discussion.ts
│   │   │   │   │   │   ├── get-discussions.ts
│   │   │   │   │   │   └── update-discussion.ts
│   │   │   │   │   └── components/
│   │   │   │   │       ├── create-discussion.tsx
│   │   │   │   │       ├── delete-discussion.tsx
│   │   │   │   │       ├── discussion-view.tsx
│   │   │   │   │       ├── discussions-list.tsx
│   │   │   │   │       └── update-discussion.tsx
│   │   │   │   ├── teams/
│   │   │   │   │   └── api/
│   │   │   │   │       └── get-teams.ts
│   │   │   │   └── users/
│   │   │   │       ├── api/
│   │   │   │       │   ├── delete-user.ts
│   │   │   │       │   ├── get-users.ts
│   │   │   │       │   └── update-profile.ts
│   │   │   │       └── components/
│   │   │   │           ├── delete-user.tsx
│   │   │   │           ├── update-profile.tsx
│   │   │   │           └── users-list.tsx
│   │   │   ├── hooks/
│   │   │   │   ├── __tests__/
│   │   │   │   │   └── use-disclosure.test.ts
│   │   │   │   └── use-disclosure.ts
│   │   │   ├── lib/
│   │   │   │   ├── __tests__/
│   │   │   │   │   └── authorization.test.tsx
│   │   │   │   ├── api-client.ts
│   │   │   │   ├── auth.tsx
│   │   │   │   ├── authorization.tsx
│   │   │   │   └── react-query.ts
│   │   │   ├── pages/
│   │   │   │   ├── 404.tsx
│   │   │   │   ├── _app.tsx
│   │   │   │   ├── app/
│   │   │   │   │   ├── discussions/
│   │   │   │   │   │   ├── [discussionId].tsx
│   │   │   │   │   │   └── index.tsx
│   │   │   │   │   ├── index.tsx
│   │   │   │   │   ├── profile.tsx
│   │   │   │   │   └── users.tsx
│   │   │   │   ├── auth/
│   │   │   │   │   ├── login.tsx
│   │   │   │   │   └── register.tsx
│   │   │   │   ├── index.tsx
│   │   │   │   └── public/
│   │   │   │       └── discussions/
│   │   │   │           └── [discussionId].tsx
│   │   │   ├── styles/
│   │   │   │   └── globals.css
│   │   │   ├── testing/
│   │   │   │   ├── data-generators.ts
│   │   │   │   ├── mocks/
│   │   │   │   │   ├── browser.ts
│   │   │   │   │   ├── db.ts
│   │   │   │   │   ├── handlers/
│   │   │   │   │   │   ├── auth.ts
│   │   │   │   │   │   ├── comments.ts
│   │   │   │   │   │   ├── discussions.ts
│   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   ├── teams.ts
│   │   │   │   │   │   └── users.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── server.ts
│   │   │   │   │   └── utils.ts
│   │   │   │   ├── setup-tests.ts
│   │   │   │   └── test-utils.tsx
│   │   │   ├── types/
│   │   │   │   └── api.ts
│   │   │   └── utils/
│   │   │       ├── cn.ts
│   │   │       └── format.ts
│   │   ├── tailwind.config.cjs
│   │   ├── tsconfig.json
│   │   └── vitest.config.ts
│   └── react-vite/
│       ├── .eslintrc.cjs
│       ├── .gitignore
│       ├── .prettierignore
│       ├── .prettierrc
│       ├── .storybook/
│       │   ├── main.ts
│       │   └── preview.tsx
│       ├── .vscode/
│       │   ├── extensions.json
│       │   └── settings.json
│       ├── README.md
│       ├── __mocks__/
│       │   ├── vitest-env.d.ts
│       │   └── zustand.ts
│       ├── e2e/
│       │   ├── .eslintrc.cjs
│       │   └── tests/
│       │       ├── auth.setup.ts
│       │       ├── profile.spec.ts
│       │       └── smoke.spec.ts
│       ├── generators/
│       │   └── component/
│       │       ├── component.stories.tsx.hbs
│       │       ├── component.tsx.hbs
│       │       ├── index.cjs
│       │       └── index.ts.hbs
│       ├── index.html
│       ├── mock-server.ts
│       ├── package.json
│       ├── playwright.config.ts
│       ├── plopfile.cjs
│       ├── postcss.config.cjs
│       ├── public/
│       │   ├── _redirects
│       │   ├── mockServiceWorker.js
│       │   └── robots.txt
│       ├── src/
│       │   ├── app/
│       │   │   ├── index.tsx
│       │   │   ├── provider.tsx
│       │   │   ├── router.tsx
│       │   │   └── routes/
│       │   │       ├── app/
│       │   │       │   ├── dashboard.tsx
│       │   │       │   ├── discussions/
│       │   │       │   │   ├── __tests__/
│       │   │       │   │   │   ├── discussion.test.tsx
│       │   │       │   │   │   └── discussions.test.tsx
│       │   │       │   │   ├── discussion.tsx
│       │   │       │   │   └── discussions.tsx
│       │   │       │   ├── profile.tsx
│       │   │       │   ├── root.tsx
│       │   │       │   └── users.tsx
│       │   │       ├── auth/
│       │   │       │   ├── login.tsx
│       │   │       │   └── register.tsx
│       │   │       ├── landing.tsx
│       │   │       └── not-found.tsx
│       │   ├── components/
│       │   │   ├── errors/
│       │   │   │   └── main.tsx
│       │   │   ├── layouts/
│       │   │   │   ├── auth-layout.tsx
│       │   │   │   ├── content-layout.tsx
│       │   │   │   ├── dashboard-layout.tsx
│       │   │   │   └── index.ts
│       │   │   ├── seo/
│       │   │   │   ├── __tests__/
│       │   │   │   │   └── head.test.tsx
│       │   │   │   ├── head.tsx
│       │   │   │   └── index.ts
│       │   │   └── ui/
│       │   │       ├── button/
│       │   │       │   ├── button.stories.tsx
│       │   │       │   ├── button.tsx
│       │   │       │   └── index.ts
│       │   │       ├── dialog/
│       │   │       │   ├── __tests__/
│       │   │       │   │   └── dialog.test.tsx
│       │   │       │   ├── confirmation-dialog/
│       │   │       │   │   ├── __tests__/
│       │   │       │   │   │   └── confirmation-dialog.test.tsx
│       │   │       │   │   ├── confirmation-dialog.stories.tsx
│       │   │       │   │   ├── confirmation-dialog.tsx
│       │   │       │   │   └── index.ts
│       │   │       │   ├── dialog.stories.tsx
│       │   │       │   ├── dialog.tsx
│       │   │       │   └── index.ts
│       │   │       ├── drawer/
│       │   │       │   ├── __tests__/
│       │   │       │   │   └── drawer.test.tsx
│       │   │       │   ├── drawer.stories.tsx
│       │   │       │   ├── drawer.tsx
│       │   │       │   └── index.ts
│       │   │       ├── dropdown/
│       │   │       │   ├── dropdown.stories.tsx
│       │   │       │   ├── dropdown.tsx
│       │   │       │   └── index.ts
│       │   │       ├── form/
│       │   │       │   ├── __tests__/
│       │   │       │   │   └── form.test.tsx
│       │   │       │   ├── error.tsx
│       │   │       │   ├── field-wrapper.tsx
│       │   │       │   ├── form-drawer.tsx
│       │   │       │   ├── form.stories.tsx
│       │   │       │   ├── form.tsx
│       │   │       │   ├── index.ts
│       │   │       │   ├── input.tsx
│       │   │       │   ├── label.tsx
│       │   │       │   ├── select.tsx
│       │   │       │   ├── switch.tsx
│       │   │       │   └── textarea.tsx
│       │   │       ├── link/
│       │   │       │   ├── index.ts
│       │   │       │   ├── link.stories.tsx
│       │   │       │   └── link.tsx
│       │   │       ├── md-preview/
│       │   │       │   ├── index.ts
│       │   │       │   ├── md-preview.stories.tsx
│       │   │       │   └── md-preview.tsx
│       │   │       ├── notifications/
│       │   │       │   ├── __tests__/
│       │   │       │   │   └── notifications.test.ts
│       │   │       │   ├── index.ts
│       │   │       │   ├── notification.stories.tsx
│       │   │       │   ├── notification.tsx
│       │   │       │   ├── notifications-store.ts
│       │   │       │   └── notifications.tsx
│       │   │       ├── spinner/
│       │   │       │   ├── index.ts
│       │   │       │   ├── spinner.stories.tsx
│       │   │       │   └── spinner.tsx
│       │   │       └── table/
│       │   │           ├── index.ts
│       │   │           ├── pagination.tsx
│       │   │           ├── table.stories.tsx
│       │   │           └── table.tsx
│       │   ├── config/
│       │   │   ├── env.ts
│       │   │   └── paths.ts
│       │   ├── features/
│       │   │   ├── auth/
│       │   │   │   └── components/
│       │   │   │       ├── __tests__/
│       │   │   │       │   ├── login-form.test.tsx
│       │   │   │       │   └── register-form.test.tsx
│       │   │   │       ├── login-form.tsx
│       │   │   │       └── register-form.tsx
│       │   │   ├── comments/
│       │   │   │   ├── api/
│       │   │   │   │   ├── create-comment.ts
│       │   │   │   │   ├── delete-comment.ts
│       │   │   │   │   └── get-comments.ts
│       │   │   │   └── components/
│       │   │   │       ├── comments-list.tsx
│       │   │   │       ├── comments.tsx
│       │   │   │       ├── create-comment.tsx
│       │   │   │       └── delete-comment.tsx
│       │   │   ├── discussions/
│       │   │   │   ├── api/
│       │   │   │   │   ├── create-discussion.ts
│       │   │   │   │   ├── delete-discussion.ts
│       │   │   │   │   ├── get-discussion.ts
│       │   │   │   │   ├── get-discussions.ts
│       │   │   │   │   └── update-discussion.ts
│       │   │   │   └── components/
│       │   │   │       ├── create-discussion.tsx
│       │   │   │       ├── delete-discussion.tsx
│       │   │   │       ├── discussion-view.tsx
│       │   │   │       ├── discussions-list.tsx
│       │   │   │       └── update-discussion.tsx
│       │   │   ├── teams/
│       │   │   │   └── api/
│       │   │   │       └── get-teams.ts
│       │   │   └── users/
│       │   │       ├── api/
│       │   │       │   ├── delete-user.ts
│       │   │       │   ├── get-users.ts
│       │   │       │   └── update-profile.ts
│       │   │       └── components/
│       │   │           ├── delete-user.tsx
│       │   │           ├── update-profile.tsx
│       │   │           └── users-list.tsx
│       │   ├── hooks/
│       │   │   ├── __tests__/
│       │   │   │   └── use-disclosure.test.ts
│       │   │   └── use-disclosure.ts
│       │   ├── index.css
│       │   ├── lib/
│       │   │   ├── __tests__/
│       │   │   │   └── authorization.test.tsx
│       │   │   ├── api-client.ts
│       │   │   ├── auth.tsx
│       │   │   ├── authorization.tsx
│       │   │   └── react-query.ts
│       │   ├── main.tsx
│       │   ├── testing/
│       │   │   ├── data-generators.ts
│       │   │   ├── mocks/
│       │   │   │   ├── browser.ts
│       │   │   │   ├── db.ts
│       │   │   │   ├── handlers/
│       │   │   │   │   ├── auth.ts
│       │   │   │   │   ├── comments.ts
│       │   │   │   │   ├── discussions.ts
│       │   │   │   │   ├── index.ts
│       │   │   │   │   ├── teams.ts
│       │   │   │   │   └── users.ts
│       │   │   │   ├── index.ts
│       │   │   │   ├── server.ts
│       │   │   │   └── utils.ts
│       │   │   ├── setup-tests.ts
│       │   │   └── test-utils.tsx
│       │   ├── types/
│       │   │   └── api.ts
│       │   ├── utils/
│       │   │   ├── cn.ts
│       │   │   └── format.ts
│       │   └── vite-env.d.ts
│       ├── tailwind.config.cjs
│       ├── tsconfig.json
│       ├── vite-env.d.ts
│       └── vite.config.ts
├── docs/
│   ├── additional-resources.md
│   ├── api-layer.md
│   ├── application-overview.md
│   ├── components-and-styling.md
│   ├── deployment.md
│   ├── error-handling.md
│   ├── performance.md
│   ├── project-standards.md
│   ├── project-structure.md
│   ├── security.md
│   ├── state-management.md
│   └── testing.md
└── package.json
Download .txt
SYMBOL INDEX (342 symbols across 210 files)

FILE: apps/nextjs-app/playwright.config.ts
  constant PORT (line 3) | const PORT = 3000;

FILE: apps/nextjs-app/public/mockServiceWorker.js
  constant PACKAGE_VERSION (line 11) | const PACKAGE_VERSION = '2.3.5'
  constant INTEGRITY_CHECKSUM (line 12) | const INTEGRITY_CHECKSUM = '26357c79639bfa20d64c0efca2a87423'
  constant IS_MOCKED_RESPONSE (line 13) | const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
  function handleRequest (line 118) | async function handleRequest(event, requestId) {
  function resolveMainClient (line 155) | async function resolveMainClient(event) {
  function getResponse (line 178) | async function getResponse(event, client, requestId) {
  function sendToClient (line 248) | function sendToClient(client, message, transferrables = []) {
  function respondWithMock (line 267) | async function respondWithMock(response) {

FILE: apps/nextjs-app/src/app/app/_components/dashboard-layout.tsx
  type SideNavigationItem (line 22) | type SideNavigationItem = {
  function Fallback (line 168) | function Fallback({ error }: { error: Error }) {

FILE: apps/nextjs-app/src/app/app/profile/_components/profile.tsx
  type EntryProps (line 6) | type EntryProps = {

FILE: apps/nextjs-app/src/app/auth/_components/auth-layout.tsx
  type LayoutProps (line 10) | type LayoutProps = {

FILE: apps/nextjs-app/src/app/provider.tsx
  type AppProviderProps (line 12) | type AppProviderProps = {

FILE: apps/nextjs-app/src/components/layouts/content-layout.tsx
  type ContentLayoutProps (line 3) | type ContentLayoutProps = {

FILE: apps/nextjs-app/src/components/ui/button/button.stories.tsx
  type Story (line 10) | type Story = StoryObj<typeof Button>;

FILE: apps/nextjs-app/src/components/ui/button/button.tsx
  type ButtonProps (line 39) | type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> &

FILE: apps/nextjs-app/src/components/ui/dialog/confirmation-dialog/confirmation-dialog.stories.tsx
  type Story (line 13) | type Story = StoryObj<typeof ConfirmationDialog>;

FILE: apps/nextjs-app/src/components/ui/dialog/confirmation-dialog/confirmation-dialog.tsx
  type ConfirmationDialogProps (line 19) | type ConfirmationDialogProps = {

FILE: apps/nextjs-app/src/components/ui/dialog/dialog.stories.tsx
  type Story (line 59) | type Story = StoryObj<typeof Dialog>;

FILE: apps/nextjs-app/src/components/ui/drawer/drawer.stories.tsx
  type Story (line 23) | type Story = StoryObj<typeof Drawer>;

FILE: apps/nextjs-app/src/components/ui/drawer/drawer.tsx
  type DrawerContentProps (line 52) | type DrawerContentProps = React.ComponentPropsWithoutRef<

FILE: apps/nextjs-app/src/components/ui/form/error.tsx
  type ErrorProps (line 1) | type ErrorProps = {

FILE: apps/nextjs-app/src/components/ui/form/field-wrapper.tsx
  type FieldWrapperProps (line 7) | type FieldWrapperProps = {
  type FieldWrapperPassThroughProps (line 14) | type FieldWrapperPassThroughProps = Omit<

FILE: apps/nextjs-app/src/components/ui/form/form-drawer.tsx
  type FormDrawerProps (line 18) | type FormDrawerProps = {

FILE: apps/nextjs-app/src/components/ui/form/form.stories.tsx
  type Story (line 66) | type Story = StoryObj<typeof MyForm>;

FILE: apps/nextjs-app/src/components/ui/form/form.tsx
  type FormFieldContextValue (line 25) | type FormFieldContextValue<
  type FormItemContextValue (line 72) | type FormItemContextValue = {
  type FormProps (line 175) | type FormProps<TFormValues extends FieldValues, Schema> = {

FILE: apps/nextjs-app/src/components/ui/form/input.tsx
  type InputProps (line 8) | type InputProps = React.InputHTMLAttributes<HTMLInputElement> &

FILE: apps/nextjs-app/src/components/ui/form/select.tsx
  type Option (line 10) | type Option = {
  type SelectFieldProps (line 15) | type SelectFieldProps = FieldWrapperPassThroughProps & {

FILE: apps/nextjs-app/src/components/ui/form/textarea.tsx
  type TextareaProps (line 8) | type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement> &

FILE: apps/nextjs-app/src/components/ui/link/link.stories.tsx
  type Story (line 11) | type Story = StoryObj<typeof Link>;

FILE: apps/nextjs-app/src/components/ui/link/link.tsx
  type LinkProps (line 5) | type LinkProps = {

FILE: apps/nextjs-app/src/components/ui/md-preview/md-preview.stories.tsx
  type Story (line 11) | type Story = StoryObj<typeof MDPreview>;

FILE: apps/nextjs-app/src/components/ui/md-preview/md-preview.tsx
  type MDPreviewProps (line 4) | type MDPreviewProps = {

FILE: apps/nextjs-app/src/components/ui/notifications/notification.stories.tsx
  type Story (line 15) | type Story = StoryObj<typeof Notification>;

FILE: apps/nextjs-app/src/components/ui/notifications/notification.tsx
  type NotificationProps (line 14) | type NotificationProps = {

FILE: apps/nextjs-app/src/components/ui/notifications/notifications-store.ts
  type Notification (line 4) | type Notification = {
  type NotificationsStore (line 11) | type NotificationsStore = {

FILE: apps/nextjs-app/src/components/ui/spinner/spinner.stories.tsx
  type Story (line 11) | type Story = StoryObj<typeof Spinner>;

FILE: apps/nextjs-app/src/components/ui/spinner/spinner.tsx
  type SpinnerProps (line 15) | type SpinnerProps = {

FILE: apps/nextjs-app/src/components/ui/table/pagination.tsx
  type PaginationLinkProps (line 43) | type PaginationLinkProps = {
  type TablePaginationProps (line 130) | type TablePaginationProps = {

FILE: apps/nextjs-app/src/components/ui/table/table.stories.tsx
  type User (line 11) | type User = {
  type Story (line 20) | type Story = StoryObj<typeof Table<User>>;

FILE: apps/nextjs-app/src/components/ui/table/table.tsx
  type TableColumn (line 126) | type TableColumn<Entry> = {
  type TableProps (line 132) | type TableProps<Entry> = {

FILE: apps/nextjs-app/src/features/auth/components/login-form.tsx
  type LoginFormProps (line 11) | type LoginFormProps = {

FILE: apps/nextjs-app/src/features/auth/components/register-form.tsx
  type RegisterFormProps (line 13) | type RegisterFormProps = {

FILE: apps/nextjs-app/src/features/comments/api/create-comment.ts
  type CreateCommentInput (line 15) | type CreateCommentInput = z.infer<typeof createCommentInputSchema>;
  type UseCreateCommentOptions (line 25) | type UseCreateCommentOptions = {

FILE: apps/nextjs-app/src/features/comments/api/delete-comment.ts
  type UseDeleteCommentOptions (line 12) | type UseDeleteCommentOptions = {

FILE: apps/nextjs-app/src/features/comments/api/get-comments.ts
  type UseCommentsOptions (line 37) | type UseCommentsOptions = {

FILE: apps/nextjs-app/src/features/comments/components/comments-list.tsx
  type CommentsListProps (line 17) | type CommentsListProps = {

FILE: apps/nextjs-app/src/features/comments/components/comments.tsx
  type CommentsProps (line 8) | type CommentsProps = {

FILE: apps/nextjs-app/src/features/comments/components/create-comment.tsx
  type CreateCommentProps (line 14) | type CreateCommentProps = {

FILE: apps/nextjs-app/src/features/comments/components/delete-comment.tsx
  type DeleteCommentProps (line 11) | type DeleteCommentProps = {

FILE: apps/nextjs-app/src/features/discussions/api/create-discussion.ts
  type CreateDiscussionInput (line 16) | type CreateDiscussionInput = z.infer<typeof createDiscussionInputSchema>;
  type UseCreateDiscussionOptions (line 26) | type UseCreateDiscussionOptions = {

FILE: apps/nextjs-app/src/features/discussions/api/delete-discussion.ts
  type UseDeleteDiscussionOptions (line 16) | type UseDeleteDiscussionOptions = {

FILE: apps/nextjs-app/src/features/discussions/api/get-discussion.ts
  type UseDiscussionOptions (line 22) | type UseDiscussionOptions = {

FILE: apps/nextjs-app/src/features/discussions/api/get-discussions.ts
  type UseDiscussionsOptions (line 29) | type UseDiscussionsOptions = {

FILE: apps/nextjs-app/src/features/discussions/api/update-discussion.ts
  type UpdateDiscussionInput (line 16) | type UpdateDiscussionInput = z.infer<typeof updateDiscussionInputSchema>;
  type UseUpdateDiscussionOptions (line 28) | type UseUpdateDiscussionOptions = {

FILE: apps/nextjs-app/src/features/discussions/components/delete-discussion.tsx
  type DeleteDiscussionProps (line 13) | type DeleteDiscussionProps = {

FILE: apps/nextjs-app/src/features/discussions/components/discussions-list.tsx
  type DiscussionsListProps (line 17) | type DiscussionsListProps = {
  method Cell (line 56) | Cell({ entry: { createdAt } }) {
  method Cell (line 63) | Cell({ entry: { id } }) {
  method Cell (line 81) | Cell({ entry: { id } }) {

FILE: apps/nextjs-app/src/features/discussions/components/update-discussion.tsx
  type UpdateDiscussionProps (line 24) | type UpdateDiscussionProps = {

FILE: apps/nextjs-app/src/features/teams/api/get-teams.ts
  type UseTeamsOptions (line 18) | type UseTeamsOptions = {

FILE: apps/nextjs-app/src/features/users/api/delete-user.ts
  type DeleteUserDTO (line 8) | type DeleteUserDTO = {
  type UseDeleteUserOptions (line 16) | type UseDeleteUserOptions = {

FILE: apps/nextjs-app/src/features/users/api/get-users.ts
  type UseUsersOptions (line 18) | type UseUsersOptions = {

FILE: apps/nextjs-app/src/features/users/api/update-profile.ts
  type UpdateProfileInput (line 15) | type UpdateProfileInput = z.infer<typeof updateProfileInputSchema>;
  type UseUpdateProfileOptions (line 21) | type UseUpdateProfileOptions = {

FILE: apps/nextjs-app/src/features/users/components/delete-user.tsx
  type DeleteUserProps (line 10) | type DeleteUserProps = {

FILE: apps/nextjs-app/src/features/users/components/users-list.tsx
  method Cell (line 49) | Cell({ entry: { createdAt } }) {
  method Cell (line 56) | Cell({ entry: { id } }) {

FILE: apps/nextjs-app/src/lib/api-client.ts
  type RequestOptions (line 4) | type RequestOptions = {
  function buildUrlWithParams (line 14) | function buildUrlWithParams(
  function getServerCookies (line 32) | function getServerCookies() {
  function fetchApi (line 50) | async function fetchApi<T>(
  method get (line 102) | get<T>(url: string, options?: RequestOptions): Promise<T> {
  method post (line 105) | post<T>(url: string, body?: any, options?: RequestOptions): Promise<T> {
  method put (line 108) | put<T>(url: string, body?: any, options?: RequestOptions): Promise<T> {
  method patch (line 111) | patch<T>(url: string, body?: any, options?: RequestOptions): Promise<T> {
  method delete (line 114) | delete<T>(url: string, options?: RequestOptions): Promise<T> {

FILE: apps/nextjs-app/src/lib/auth.tsx
  type LoginInput (line 75) | type LoginInput = z.infer<typeof loginInputSchema>;
  type RegisterInput (line 101) | type RegisterInput = z.infer<typeof registerInputSchema>;

FILE: apps/nextjs-app/src/lib/react-query.ts
  type ApiFnReturnType (line 12) | type ApiFnReturnType<FnType extends (...args: any) => Promise<any>> =
  type QueryConfig (line 15) | type QueryConfig<T extends (...args: any[]) => any> = Omit<
  type MutationConfig (line 20) | type MutationConfig<

FILE: apps/nextjs-app/src/testing/mocks/db.ts
  type Model (line 42) | type Model = keyof typeof models;

FILE: apps/nextjs-app/src/testing/mocks/handlers/auth.ts
  type RegisterBody (line 15) | type RegisterBody = {
  type LoginBody (line 24) | type LoginBody = {

FILE: apps/nextjs-app/src/testing/mocks/handlers/comments.ts
  type CreateCommentBody (line 8) | type CreateCommentBody = {

FILE: apps/nextjs-app/src/testing/mocks/handlers/discussions.ts
  type DiscussionBody (line 13) | type DiscussionBody = {

FILE: apps/nextjs-app/src/testing/mocks/handlers/users.ts
  type ProfileBody (line 13) | type ProfileBody = {

FILE: apps/nextjs-app/src/testing/mocks/utils.ts
  function authenticate (line 53) | function authenticate({
  constant AUTH_COOKIE (line 78) | const AUTH_COOKIE = `bulletproof_react_app_token`;
  function requireAuth (line 80) | function requireAuth(cookies: Record<string, string>) {
  function requireAdmin (line 106) | function requireAdmin(user: any) {

FILE: apps/nextjs-app/src/types/api.ts
  type BaseEntity (line 5) | type BaseEntity = {
  type Entity (line 10) | type Entity<T> = {
  type Meta (line 14) | type Meta = {
  type User (line 20) | type User = Entity<{
  type AuthResponse (line 29) | type AuthResponse = {
  type Team (line 34) | type Team = Entity<{
  type Discussion (line 39) | type Discussion = Entity<{
  type Comment (line 47) | type Comment = Entity<{

FILE: apps/nextjs-app/src/utils/auth.ts
  constant AUTH_TOKEN_COOKIE_NAME (line 3) | const AUTH_TOKEN_COOKIE_NAME = 'bulletproof_react_app_token';

FILE: apps/nextjs-app/src/utils/cn.ts
  function cn (line 4) | function cn(...inputs: ClassValue[]) {

FILE: apps/nextjs-pages/playwright.config.ts
  constant PORT (line 3) | const PORT = 3000;

FILE: apps/nextjs-pages/public/mockServiceWorker.js
  constant PACKAGE_VERSION (line 11) | const PACKAGE_VERSION = '2.3.5'
  constant INTEGRITY_CHECKSUM (line 12) | const INTEGRITY_CHECKSUM = '26357c79639bfa20d64c0efca2a87423'
  constant IS_MOCKED_RESPONSE (line 13) | const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
  function handleRequest (line 118) | async function handleRequest(event, requestId) {
  function resolveMainClient (line 155) | async function resolveMainClient(event) {
  function getResponse (line 178) | async function getResponse(event, client, requestId) {
  function sendToClient (line 248) | function sendToClient(client, message, transferrables = []) {
  function respondWithMock (line 267) | async function respondWithMock(response) {

FILE: apps/nextjs-pages/src/app/pages/app/discussions/discussion.tsx
  type DiscussionPageProps (line 21) | type DiscussionPageProps = {

FILE: apps/nextjs-pages/src/app/pages/app/profile.tsx
  type EntryProps (line 7) | type EntryProps = {

FILE: apps/nextjs-pages/src/app/provider.tsx
  type AppProviderProps (line 11) | type AppProviderProps = {

FILE: apps/nextjs-pages/src/components/layouts/auth-layout.tsx
  type LayoutProps (line 10) | type LayoutProps = {

FILE: apps/nextjs-pages/src/components/layouts/content-layout.tsx
  type ContentLayoutProps (line 5) | type ContentLayoutProps = {

FILE: apps/nextjs-pages/src/components/layouts/dashboard-layout.tsx
  type SideNavigationItem (line 24) | type SideNavigationItem = {

FILE: apps/nextjs-pages/src/components/seo/head.tsx
  type HeadProps (line 3) | type HeadProps = {

FILE: apps/nextjs-pages/src/components/ui/button/button.stories.tsx
  type Story (line 10) | type Story = StoryObj<typeof Button>;

FILE: apps/nextjs-pages/src/components/ui/button/button.tsx
  type ButtonProps (line 39) | type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> &

FILE: apps/nextjs-pages/src/components/ui/dialog/confirmation-dialog/confirmation-dialog.stories.tsx
  type Story (line 13) | type Story = StoryObj<typeof ConfirmationDialog>;

FILE: apps/nextjs-pages/src/components/ui/dialog/confirmation-dialog/confirmation-dialog.tsx
  type ConfirmationDialogProps (line 17) | type ConfirmationDialogProps = {

FILE: apps/nextjs-pages/src/components/ui/dialog/dialog.stories.tsx
  type Story (line 59) | type Story = StoryObj<typeof Dialog>;

FILE: apps/nextjs-pages/src/components/ui/drawer/drawer.stories.tsx
  type Story (line 23) | type Story = StoryObj<typeof Drawer>;

FILE: apps/nextjs-pages/src/components/ui/drawer/drawer.tsx
  type DrawerContentProps (line 50) | type DrawerContentProps = React.ComponentPropsWithoutRef<

FILE: apps/nextjs-pages/src/components/ui/form/error.tsx
  type ErrorProps (line 1) | type ErrorProps = {

FILE: apps/nextjs-pages/src/components/ui/form/field-wrapper.tsx
  type FieldWrapperProps (line 7) | type FieldWrapperProps = {
  type FieldWrapperPassThroughProps (line 14) | type FieldWrapperPassThroughProps = Omit<

FILE: apps/nextjs-pages/src/components/ui/form/form-drawer.tsx
  type FormDrawerProps (line 16) | type FormDrawerProps = {

FILE: apps/nextjs-pages/src/components/ui/form/form.stories.tsx
  type Story (line 66) | type Story = StoryObj<typeof MyForm>;

FILE: apps/nextjs-pages/src/components/ui/form/form.tsx
  type FormFieldContextValue (line 23) | type FormFieldContextValue<
  type FormItemContextValue (line 70) | type FormItemContextValue = {
  type FormProps (line 173) | type FormProps<TFormValues extends FieldValues, Schema> = {

FILE: apps/nextjs-pages/src/components/ui/form/input.tsx
  type InputProps (line 8) | type InputProps = React.InputHTMLAttributes<HTMLInputElement> &

FILE: apps/nextjs-pages/src/components/ui/form/select.tsx
  type Option (line 8) | type Option = {
  type SelectFieldProps (line 13) | type SelectFieldProps = FieldWrapperPassThroughProps & {

FILE: apps/nextjs-pages/src/components/ui/form/textarea.tsx
  type TextareaProps (line 8) | type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement> &

FILE: apps/nextjs-pages/src/components/ui/link/link.stories.tsx
  type Story (line 11) | type Story = StoryObj<typeof Link>;

FILE: apps/nextjs-pages/src/components/ui/link/link.tsx
  type LinkProps (line 5) | type LinkProps = {

FILE: apps/nextjs-pages/src/components/ui/md-preview/md-preview.stories.tsx
  type Story (line 11) | type Story = StoryObj<typeof MDPreview>;

FILE: apps/nextjs-pages/src/components/ui/md-preview/md-preview.tsx
  type MDPreviewProps (line 4) | type MDPreviewProps = {

FILE: apps/nextjs-pages/src/components/ui/notifications/notification.stories.tsx
  type Story (line 15) | type Story = StoryObj<typeof Notification>;

FILE: apps/nextjs-pages/src/components/ui/notifications/notification.tsx
  type NotificationProps (line 12) | type NotificationProps = {

FILE: apps/nextjs-pages/src/components/ui/notifications/notifications-store.ts
  type Notification (line 4) | type Notification = {
  type NotificationsStore (line 11) | type NotificationsStore = {

FILE: apps/nextjs-pages/src/components/ui/spinner/spinner.stories.tsx
  type Story (line 11) | type Story = StoryObj<typeof Spinner>;

FILE: apps/nextjs-pages/src/components/ui/spinner/spinner.tsx
  type SpinnerProps (line 15) | type SpinnerProps = {

FILE: apps/nextjs-pages/src/components/ui/table/pagination.tsx
  type PaginationLinkProps (line 43) | type PaginationLinkProps = {
  type TablePaginationProps (line 130) | type TablePaginationProps = {

FILE: apps/nextjs-pages/src/components/ui/table/table.stories.tsx
  type User (line 11) | type User = {
  type Story (line 20) | type Story = StoryObj<typeof Table<User>>;

FILE: apps/nextjs-pages/src/components/ui/table/table.tsx
  type TableColumn (line 126) | type TableColumn<Entry> = {
  type TableProps (line 132) | type TableProps<Entry> = {

FILE: apps/nextjs-pages/src/features/auth/components/login-form.tsx
  type LoginFormProps (line 9) | type LoginFormProps = {

FILE: apps/nextjs-pages/src/features/auth/components/register-form.tsx
  type RegisterFormProps (line 11) | type RegisterFormProps = {

FILE: apps/nextjs-pages/src/features/comments/api/create-comment.ts
  type CreateCommentInput (line 15) | type CreateCommentInput = z.infer<typeof createCommentInputSchema>;
  type UseCreateCommentOptions (line 25) | type UseCreateCommentOptions = {

FILE: apps/nextjs-pages/src/features/comments/api/delete-comment.ts
  type UseDeleteCommentOptions (line 12) | type UseDeleteCommentOptions = {

FILE: apps/nextjs-pages/src/features/comments/api/get-comments.ts
  type UseCommentsOptions (line 43) | type UseCommentsOptions = {

FILE: apps/nextjs-pages/src/features/comments/components/comments-list.tsx
  type CommentsListProps (line 16) | type CommentsListProps = {

FILE: apps/nextjs-pages/src/features/comments/components/comments.tsx
  type CommentsProps (line 6) | type CommentsProps = {

FILE: apps/nextjs-pages/src/features/comments/components/create-comment.tsx
  type CreateCommentProps (line 12) | type CreateCommentProps = {

FILE: apps/nextjs-pages/src/features/comments/components/delete-comment.tsx
  type DeleteCommentProps (line 9) | type DeleteCommentProps = {

FILE: apps/nextjs-pages/src/features/discussions/api/create-discussion.ts
  type CreateDiscussionInput (line 16) | type CreateDiscussionInput = z.infer<typeof createDiscussionInputSchema>;
  type UseCreateDiscussionOptions (line 26) | type UseCreateDiscussionOptions = {

FILE: apps/nextjs-pages/src/features/discussions/api/delete-discussion.ts
  type UseDeleteDiscussionOptions (line 16) | type UseDeleteDiscussionOptions = {

FILE: apps/nextjs-pages/src/features/discussions/api/get-discussion.ts
  type UseDiscussionOptions (line 29) | type UseDiscussionOptions = {

FILE: apps/nextjs-pages/src/features/discussions/api/get-discussions.ts
  type UseDiscussionsOptions (line 31) | type UseDiscussionsOptions = {

FILE: apps/nextjs-pages/src/features/discussions/api/update-discussion.ts
  type UpdateDiscussionInput (line 16) | type UpdateDiscussionInput = z.infer<typeof updateDiscussionInputSchema>;
  type UseUpdateDiscussionOptions (line 28) | type UseUpdateDiscussionOptions = {

FILE: apps/nextjs-pages/src/features/discussions/components/delete-discussion.tsx
  type DeleteDiscussionProps (line 10) | type DeleteDiscussionProps = {

FILE: apps/nextjs-pages/src/features/discussions/components/discussions-list.tsx
  type DiscussionsListProps (line 15) | type DiscussionsListProps = {
  method Cell (line 54) | Cell({ entry: { createdAt } }) {
  method Cell (line 61) | Cell({ entry: { id } }) {
  method Cell (line 79) | Cell({ entry: { id } }) {

FILE: apps/nextjs-pages/src/features/discussions/components/update-discussion.tsx
  type UpdateDiscussionProps (line 21) | type UpdateDiscussionProps = {

FILE: apps/nextjs-pages/src/features/teams/api/get-teams.ts
  type UseTeamsOptions (line 18) | type UseTeamsOptions = {

FILE: apps/nextjs-pages/src/features/users/api/delete-user.ts
  type DeleteUserDTO (line 8) | type DeleteUserDTO = {
  type UseDeleteUserOptions (line 16) | type UseDeleteUserOptions = {

FILE: apps/nextjs-pages/src/features/users/api/get-users.ts
  type UseUsersOptions (line 18) | type UseUsersOptions = {

FILE: apps/nextjs-pages/src/features/users/api/update-profile.ts
  type UpdateProfileInput (line 15) | type UpdateProfileInput = z.infer<typeof updateProfileInputSchema>;
  type UseUpdateProfileOptions (line 21) | type UseUpdateProfileOptions = {

FILE: apps/nextjs-pages/src/features/users/components/delete-user.tsx
  type DeleteUserProps (line 8) | type DeleteUserProps = {

FILE: apps/nextjs-pages/src/features/users/components/users-list.tsx
  method Cell (line 47) | Cell({ entry: { createdAt } }) {
  method Cell (line 54) | Cell({ entry: { id } }) {

FILE: apps/nextjs-pages/src/lib/api-client.ts
  function authRequestInterceptor (line 7) | function authRequestInterceptor(config: InternalAxiosRequestConfig) {

FILE: apps/nextjs-pages/src/lib/auth.tsx
  type LoginInput (line 29) | type LoginInput = z.infer<typeof loginInputSchema>;
  type RegisterInput (line 55) | type RegisterInput = z.infer<typeof registerInputSchema>;

FILE: apps/nextjs-pages/src/lib/authorization.tsx
  type ROLES (line 8) | enum ROLES {
  type RoleTypes (line 13) | type RoleTypes = keyof typeof ROLES;
  constant POLICIES (line 15) | const POLICIES = {
  type AuthorizationProps (line 52) | type AuthorizationProps = {

FILE: apps/nextjs-pages/src/lib/react-query.ts
  type ApiFnReturnType (line 12) | type ApiFnReturnType<FnType extends (...args: any) => Promise<any>> =
  type QueryConfig (line 15) | type QueryConfig<T extends (...args: any[]) => any> = Omit<
  type MutationConfig (line 20) | type MutationConfig<

FILE: apps/nextjs-pages/src/pages/_app.tsx
  type NextPageWithLayout (line 10) | type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
  type AppPropsWithLayout (line 14) | type AppPropsWithLayout = AppProps & {
  function App (line 18) | function App({ Component, pageProps }: AppPropsWithLayout) {

FILE: apps/nextjs-pages/src/testing/mocks/db.ts
  type Model (line 42) | type Model = keyof typeof models;

FILE: apps/nextjs-pages/src/testing/mocks/handlers/auth.ts
  type RegisterBody (line 15) | type RegisterBody = {
  type LoginBody (line 24) | type LoginBody = {

FILE: apps/nextjs-pages/src/testing/mocks/handlers/comments.ts
  type CreateCommentBody (line 8) | type CreateCommentBody = {

FILE: apps/nextjs-pages/src/testing/mocks/handlers/discussions.ts
  type DiscussionBody (line 13) | type DiscussionBody = {

FILE: apps/nextjs-pages/src/testing/mocks/handlers/users.ts
  type ProfileBody (line 13) | type ProfileBody = {

FILE: apps/nextjs-pages/src/testing/mocks/utils.ts
  function authenticate (line 53) | function authenticate({
  constant AUTH_COOKIE (line 78) | const AUTH_COOKIE = `bulletproof_react_app_token`;
  function requireAuth (line 80) | function requireAuth(cookies: Record<string, string>) {
  function requireAdmin (line 106) | function requireAdmin(user: any) {

FILE: apps/nextjs-pages/src/types/api.ts
  type BaseEntity (line 5) | type BaseEntity = {
  type Entity (line 10) | type Entity<T> = {
  type Meta (line 14) | type Meta = {
  type User (line 20) | type User = Entity<{
  type AuthResponse (line 29) | type AuthResponse = {
  type Team (line 34) | type Team = Entity<{
  type Discussion (line 39) | type Discussion = Entity<{
  type Comment (line 47) | type Comment = Entity<{

FILE: apps/nextjs-pages/src/utils/cn.ts
  function cn (line 4) | function cn(...inputs: ClassValue[]) {

FILE: apps/react-vite/playwright.config.ts
  constant PORT (line 3) | const PORT = 3000;

FILE: apps/react-vite/public/mockServiceWorker.js
  constant PACKAGE_VERSION (line 11) | const PACKAGE_VERSION = '2.2.14'
  constant INTEGRITY_CHECKSUM (line 12) | const INTEGRITY_CHECKSUM = '26357c79639bfa20d64c0efca2a87423'
  constant IS_MOCKED_RESPONSE (line 13) | const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
  function handleRequest (line 118) | async function handleRequest(event, requestId) {
  function resolveMainClient (line 155) | async function resolveMainClient(event) {
  function getResponse (line 178) | async function getResponse(event, client, requestId) {
  function sendToClient (line 248) | function sendToClient(client, message, transferrables = []) {
  function respondWithMock (line 267) | async function respondWithMock(response) {

FILE: apps/react-vite/src/app/provider.tsx
  type AppProviderProps (line 13) | type AppProviderProps = {

FILE: apps/react-vite/src/app/routes/app/profile.tsx
  type EntryProps (line 5) | type EntryProps = {

FILE: apps/react-vite/src/components/layouts/auth-layout.tsx
  type LayoutProps (line 11) | type LayoutProps = {

FILE: apps/react-vite/src/components/layouts/content-layout.tsx
  type ContentLayoutProps (line 5) | type ContentLayoutProps = {

FILE: apps/react-vite/src/components/layouts/dashboard-layout.tsx
  type SideNavigationItem (line 22) | type SideNavigationItem = {
  function DashboardLayout (line 79) | function DashboardLayout({ children }: { children: React.ReactNode }) {

FILE: apps/react-vite/src/components/seo/head.tsx
  type HeadProps (line 3) | type HeadProps = {

FILE: apps/react-vite/src/components/ui/button/button.stories.tsx
  type Story (line 10) | type Story = StoryObj<typeof Button>;

FILE: apps/react-vite/src/components/ui/button/button.tsx
  type ButtonProps (line 39) | type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> &

FILE: apps/react-vite/src/components/ui/dialog/confirmation-dialog/confirmation-dialog.stories.tsx
  type Story (line 13) | type Story = StoryObj<typeof ConfirmationDialog>;

FILE: apps/react-vite/src/components/ui/dialog/confirmation-dialog/confirmation-dialog.tsx
  type ConfirmationDialogProps (line 17) | type ConfirmationDialogProps = {

FILE: apps/react-vite/src/components/ui/dialog/dialog.stories.tsx
  type Story (line 59) | type Story = StoryObj<typeof Dialog>;

FILE: apps/react-vite/src/components/ui/drawer/drawer.stories.tsx
  type Story (line 23) | type Story = StoryObj<typeof Drawer>;

FILE: apps/react-vite/src/components/ui/drawer/drawer.tsx
  type DrawerContentProps (line 50) | type DrawerContentProps = React.ComponentPropsWithoutRef<

FILE: apps/react-vite/src/components/ui/form/error.tsx
  type ErrorProps (line 1) | type ErrorProps = {

FILE: apps/react-vite/src/components/ui/form/field-wrapper.tsx
  type FieldWrapperProps (line 7) | type FieldWrapperProps = {
  type FieldWrapperPassThroughProps (line 14) | type FieldWrapperPassThroughProps = Omit<

FILE: apps/react-vite/src/components/ui/form/form-drawer.tsx
  type FormDrawerProps (line 16) | type FormDrawerProps = {

FILE: apps/react-vite/src/components/ui/form/form.stories.tsx
  type Story (line 66) | type Story = StoryObj<typeof MyForm>;

FILE: apps/react-vite/src/components/ui/form/form.tsx
  type FormFieldContextValue (line 23) | type FormFieldContextValue<
  type FormItemContextValue (line 70) | type FormItemContextValue = {
  type FormProps (line 173) | type FormProps<TFormValues extends FieldValues, Schema> = {

FILE: apps/react-vite/src/components/ui/form/input.tsx
  type InputProps (line 8) | type InputProps = React.InputHTMLAttributes<HTMLInputElement> &

FILE: apps/react-vite/src/components/ui/form/select.tsx
  type Option (line 8) | type Option = {
  type SelectFieldProps (line 13) | type SelectFieldProps = FieldWrapperPassThroughProps & {

FILE: apps/react-vite/src/components/ui/form/textarea.tsx
  type TextareaProps (line 8) | type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement> &

FILE: apps/react-vite/src/components/ui/link/link.stories.tsx
  type Story (line 11) | type Story = StoryObj<typeof Link>;

FILE: apps/react-vite/src/components/ui/md-preview/md-preview.stories.tsx
  type Story (line 11) | type Story = StoryObj<typeof MDPreview>;

FILE: apps/react-vite/src/components/ui/md-preview/md-preview.tsx
  type MDPreviewProps (line 6) | type MDPreviewProps = {

FILE: apps/react-vite/src/components/ui/notifications/notification.stories.tsx
  type Story (line 15) | type Story = StoryObj<typeof Notification>;

FILE: apps/react-vite/src/components/ui/notifications/notification.tsx
  type NotificationProps (line 12) | type NotificationProps = {

FILE: apps/react-vite/src/components/ui/notifications/notifications-store.ts
  type Notification (line 4) | type Notification = {
  type NotificationsStore (line 11) | type NotificationsStore = {

FILE: apps/react-vite/src/components/ui/spinner/spinner.stories.tsx
  type Story (line 11) | type Story = StoryObj<typeof Spinner>;

FILE: apps/react-vite/src/components/ui/spinner/spinner.tsx
  type SpinnerProps (line 15) | type SpinnerProps = {

FILE: apps/react-vite/src/components/ui/table/pagination.tsx
  type PaginationLinkProps (line 43) | type PaginationLinkProps = {
  type TablePaginationProps (line 130) | type TablePaginationProps = {

FILE: apps/react-vite/src/components/ui/table/table.stories.tsx
  type User (line 11) | type User = {
  type Story (line 20) | type Story = StoryObj<typeof Table<User>>;

FILE: apps/react-vite/src/components/ui/table/table.tsx
  type TableColumn (line 126) | type TableColumn<Entry> = {
  type TableProps (line 132) | type TableProps<Entry> = {

FILE: apps/react-vite/src/features/auth/components/login-form.tsx
  type LoginFormProps (line 8) | type LoginFormProps = {

FILE: apps/react-vite/src/features/auth/components/register-form.tsx
  type RegisterFormProps (line 10) | type RegisterFormProps = {

FILE: apps/react-vite/src/features/comments/api/create-comment.ts
  type CreateCommentInput (line 15) | type CreateCommentInput = z.infer<typeof createCommentInputSchema>;
  type UseCreateCommentOptions (line 25) | type UseCreateCommentOptions = {

FILE: apps/react-vite/src/features/comments/api/delete-comment.ts
  type UseDeleteCommentOptions (line 12) | type UseDeleteCommentOptions = {

FILE: apps/react-vite/src/features/comments/api/get-comments.ts
  type UseCommentsOptions (line 37) | type UseCommentsOptions = {

FILE: apps/react-vite/src/features/comments/components/comments-list.tsx
  type CommentsListProps (line 15) | type CommentsListProps = {

FILE: apps/react-vite/src/features/comments/components/comments.tsx
  type CommentsProps (line 4) | type CommentsProps = {

FILE: apps/react-vite/src/features/comments/components/create-comment.tsx
  type CreateCommentProps (line 12) | type CreateCommentProps = {

FILE: apps/react-vite/src/features/comments/components/delete-comment.tsx
  type DeleteCommentProps (line 9) | type DeleteCommentProps = {

FILE: apps/react-vite/src/features/discussions/api/create-discussion.ts
  type CreateDiscussionInput (line 15) | type CreateDiscussionInput = z.infer<typeof createDiscussionInputSchema>;
  type UseCreateDiscussionOptions (line 25) | type UseCreateDiscussionOptions = {

FILE: apps/react-vite/src/features/discussions/api/delete-discussion.ts
  type UseDeleteDiscussionOptions (line 16) | type UseDeleteDiscussionOptions = {

FILE: apps/react-vite/src/features/discussions/api/get-discussion.ts
  type UseDiscussionOptions (line 22) | type UseDiscussionOptions = {

FILE: apps/react-vite/src/features/discussions/api/get-discussions.ts
  type UseDiscussionsOptions (line 29) | type UseDiscussionsOptions = {

FILE: apps/react-vite/src/features/discussions/api/update-discussion.ts
  type UpdateDiscussionInput (line 15) | type UpdateDiscussionInput = z.infer<typeof updateDiscussionInputSchema>;
  type UseUpdateDiscussionOptions (line 27) | type UseUpdateDiscussionOptions = {

FILE: apps/react-vite/src/features/discussions/components/delete-discussion.tsx
  type DeleteDiscussionProps (line 10) | type DeleteDiscussionProps = {

FILE: apps/react-vite/src/features/discussions/components/discussions-list.tsx
  type DiscussionsListProps (line 15) | type DiscussionsListProps = {
  method Cell (line 53) | Cell({ entry: { createdAt } }) {
  method Cell (line 60) | Cell({ entry: { id } }) {
  method Cell (line 78) | Cell({ entry: { id } }) {

FILE: apps/react-vite/src/features/discussions/components/update-discussion.tsx
  type UpdateDiscussionProps (line 14) | type UpdateDiscussionProps = {

FILE: apps/react-vite/src/features/teams/api/get-teams.ts
  type UseTeamsOptions (line 18) | type UseTeamsOptions = {

FILE: apps/react-vite/src/features/users/api/delete-user.ts
  type DeleteUserDTO (line 8) | type DeleteUserDTO = {
  type UseDeleteUserOptions (line 16) | type UseDeleteUserOptions = {

FILE: apps/react-vite/src/features/users/api/get-users.ts
  type UseUsersOptions (line 18) | type UseUsersOptions = {

FILE: apps/react-vite/src/features/users/api/update-profile.ts
  type UpdateProfileInput (line 15) | type UpdateProfileInput = z.infer<typeof updateProfileInputSchema>;
  type UseUpdateProfileOptions (line 21) | type UseUpdateProfileOptions = {

FILE: apps/react-vite/src/features/users/components/delete-user.tsx
  type DeleteUserProps (line 8) | type DeleteUserProps = {

FILE: apps/react-vite/src/features/users/components/users-list.tsx
  method Cell (line 47) | Cell({ entry: { createdAt } }) {
  method Cell (line 54) | Cell({ entry: { id } }) {

FILE: apps/react-vite/src/lib/api-client.ts
  function authRequestInterceptor (line 7) | function authRequestInterceptor(config: InternalAxiosRequestConfig) {

FILE: apps/react-vite/src/lib/auth.tsx
  type LoginInput (line 28) | type LoginInput = z.infer<typeof loginInputSchema>;
  type RegisterInput (line 54) | type RegisterInput = z.infer<typeof registerInputSchema>;

FILE: apps/react-vite/src/lib/authorization.tsx
  type ROLES (line 7) | enum ROLES {
  type RoleTypes (line 12) | type RoleTypes = keyof typeof ROLES;
  constant POLICIES (line 14) | const POLICIES = {
  type AuthorizationProps (line 49) | type AuthorizationProps = {

FILE: apps/react-vite/src/lib/react-query.ts
  type ApiFnReturnType (line 12) | type ApiFnReturnType<FnType extends (...args: any) => Promise<any>> =
  type QueryConfig (line 15) | type QueryConfig<T extends (...args: any[]) => any> = Omit<
  type MutationConfig (line 20) | type MutationConfig<

FILE: apps/react-vite/src/testing/mocks/db.ts
  type Model (line 41) | type Model = keyof typeof models;

FILE: apps/react-vite/src/testing/mocks/handlers/auth.ts
  type RegisterBody (line 15) | type RegisterBody = {
  type LoginBody (line 24) | type LoginBody = {

FILE: apps/react-vite/src/testing/mocks/handlers/comments.ts
  type CreateCommentBody (line 8) | type CreateCommentBody = {

FILE: apps/react-vite/src/testing/mocks/handlers/discussions.ts
  type DiscussionBody (line 13) | type DiscussionBody = {

FILE: apps/react-vite/src/testing/mocks/handlers/users.ts
  type ProfileBody (line 13) | type ProfileBody = {

FILE: apps/react-vite/src/testing/mocks/utils.ts
  function authenticate (line 53) | function authenticate({
  constant AUTH_COOKIE (line 78) | const AUTH_COOKIE = `bulletproof_react_app_token`;
  function requireAuth (line 80) | function requireAuth(cookies: Record<string, string>) {
  function requireAdmin (line 106) | function requireAdmin(user: any) {

FILE: apps/react-vite/src/types/api.ts
  type BaseEntity (line 5) | type BaseEntity = {
  type Entity (line 10) | type Entity<T> = {
  type Meta (line 14) | type Meta = {
  type User (line 20) | type User = Entity<{
  type AuthResponse (line 29) | type AuthResponse = {
  type Team (line 34) | type Team = Entity<{
  type Discussion (line 39) | type Discussion = Entity<{
  type Comment (line 46) | type Comment = Entity<{

FILE: apps/react-vite/src/utils/cn.ts
  function cn (line 4) | function cn(...inputs: ClassValue[]) {
Condensed preview — 511 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (786K chars).
[
  {
    "path": ".github/workflows/nextjs-app-ci.yml",
    "chars": 1567,
    "preview": "name: Next.js App CI\non:\n  push:\n    branches: [\"*\"]\n    paths-ignore:\n      - \"README.md\"\n      - \"docs/**\"\n  pull_requ"
  },
  {
    "path": ".github/workflows/nextjs-pages-ci.yml",
    "chars": 1573,
    "preview": "name: Next.js Pages CI\non:\n  push:\n    branches: [\"*\"]\n    paths-ignore:\n      - \"README.md\"\n      - \"docs/**\"\n  pull_re"
  },
  {
    "path": ".github/workflows/react-vite-ci.yml",
    "chars": 1566,
    "preview": "name: React Vite CI\non:\n  push:\n    branches: [\"*\"]\n    paths-ignore:\n      - \"README.md\"\n      - \"docs/**\"\n  pull_reque"
  },
  {
    "path": ".gitignore",
    "chars": 60,
    "preview": "# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# misc\n.DS_Store"
  },
  {
    "path": ".husky/pre-commit",
    "chars": 124,
    "preview": "yarn --cwd apps/nextjs-app lint-staged && yarn --cwd apps/nextjs-pages lint-staged && yarn --cwd apps/react-vite lint-st"
  },
  {
    "path": "LICENSE",
    "chars": 1071,
    "preview": "MIT License\n\nCopyright (c) 2024 Alan Alickovic\n\nPermission is hereby granted, free of charge, to any person obtaining a "
  },
  {
    "path": "README.md",
    "chars": 4416,
    "preview": "# Bulletproof React 🛡️ ⚛️\n\n[![MIT License](https://img.shields.io/github/license/alan2207/bulletproof-react)](https://gi"
  },
  {
    "path": "apps/nextjs-app/.eslintrc.cjs",
    "chars": 4597,
    "preview": "module.exports = {\n  root: true,\n  env: {\n    node: true,\n    es6: true,\n  },\n  parserOptions: { ecmaVersion: 'latest', "
  },
  {
    "path": "apps/nextjs-app/.gitignore",
    "chars": 524,
    "preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
  },
  {
    "path": "apps/nextjs-app/.prettierignore",
    "chars": 5,
    "preview": "*.hbs"
  },
  {
    "path": "apps/nextjs-app/.prettierrc",
    "chars": 109,
    "preview": "{\n  \"singleQuote\": true,\n  \"trailingComma\": \"all\",\n  \"printWidth\": 80,\n  \"tabWidth\": 2,\n  \"useTabs\": false\n}\n"
  },
  {
    "path": "apps/nextjs-app/.storybook/main.ts",
    "chars": 477,
    "preview": "module.exports = {\n  stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],\n\n  addons: [\n    '@st"
  },
  {
    "path": "apps/nextjs-app/.storybook/preview.tsx",
    "chars": 190,
    "preview": "import React from 'react';\nimport '../src/styles/globals.css';\n\nexport const parameters = {\n  actions: { argTypesRegex: "
  },
  {
    "path": "apps/nextjs-app/.vscode/extensions.json",
    "chars": 200,
    "preview": "{\n  \"recommendations\": [\n    \"dbaeumer.vscode-eslint\",\n    \"esbenp.prettier-vscode\",\n    \"dsznajder.es7-react-js-snippet"
  },
  {
    "path": "apps/nextjs-app/.vscode/settings.json",
    "chars": 110,
    "preview": "{\n  \"editor.formatOnSave\": true,\n  \"editor.codeActionsOnSave\": {\n    \"source.fixAll.eslint\": \"explicit\"\n  }\n}\n"
  },
  {
    "path": "apps/nextjs-app/README.md",
    "chars": 587,
    "preview": "# Next.js App Application\n\n## Get Started\n\nPrerequisites:\n\n- Node 20+\n- Yarn 1.22+\n\nTo set up the app execute the follow"
  },
  {
    "path": "apps/nextjs-app/__mocks__/vitest-env.d.ts",
    "chars": 79,
    "preview": "/// <reference types=\"vite/client\" />\n/// <reference types=\"vitest/globals\" />\n"
  },
  {
    "path": "apps/nextjs-app/__mocks__/zustand.ts",
    "chars": 1754,
    "preview": "import { act } from '@testing-library/react';\nimport { afterEach, vi } from 'vitest';\nimport * as zustand from 'zustand'"
  },
  {
    "path": "apps/nextjs-app/e2e/.eslintrc.cjs",
    "chars": 119,
    "preview": "module.exports = {\n  root: true,\n  parser: '@typescript-eslint/parser',\n  extends: 'plugin:playwright/recommended',\n};\n"
  },
  {
    "path": "apps/nextjs-app/e2e/tests/auth.setup.ts",
    "chars": 1665,
    "preview": "import { test as setup, expect } from '@playwright/test';\nimport { createUser } from '../../src/testing/data-generators'"
  },
  {
    "path": "apps/nextjs-app/e2e/tests/profile.spec.ts",
    "chars": 658,
    "preview": "import { test, expect } from '@playwright/test';\n\ntest('profile', async ({ page }) => {\n  // update user:\n  await page.g"
  },
  {
    "path": "apps/nextjs-app/e2e/tests/smoke.spec.ts",
    "chars": 3340,
    "preview": "import { test, expect } from '@playwright/test';\n\nimport {\n  createDiscussion,\n  createComment,\n} from '../../src/testin"
  },
  {
    "path": "apps/nextjs-app/generators/component/component.stories.tsx.hbs",
    "chars": 330,
    "preview": "import { Meta, StoryObj } from '@storybook/react';\n\nimport { {{ properCase name }} } from './{{ kebabCase name }}';\n\ncon"
  },
  {
    "path": "apps/nextjs-app/generators/component/component.tsx.hbs",
    "chars": 219,
    "preview": "import * as React from \"react\"; \n\nexport type {{properCase name}}Props = {};\n\nexport const {{properCase name}} = (props:"
  },
  {
    "path": "apps/nextjs-app/generators/component/index.cjs",
    "chars": 1532,
    "preview": "const path = require('path');\nconst fs = require('fs');\n\nconst featuresDir = path.join(process.cwd(), 'src/features');\nc"
  },
  {
    "path": "apps/nextjs-app/generators/component/index.ts.hbs",
    "chars": 40,
    "preview": "export * from './{{ kebabCase name }}';\n"
  },
  {
    "path": "apps/nextjs-app/index.html",
    "chars": 610,
    "preview": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <link rel=\"icon\" href=\"/favicon.ico\" />\n    <"
  },
  {
    "path": "apps/nextjs-app/lint-staged.config.mjs",
    "chars": 345,
    "preview": "import path from 'path';\n\nconst buildEslintCommand = (filenames) => {\n  return `next lint --fix --file ${filenames\n    ."
  },
  {
    "path": "apps/nextjs-app/mock-server.ts",
    "chars": 921,
    "preview": "import { createMiddleware } from '@mswjs/http-middleware';\nimport cors from 'cors';\nimport express from 'express';\nimpor"
  },
  {
    "path": "apps/nextjs-app/next-env.d.ts",
    "chars": 201,
    "preview": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\n\n// NOTE: This file should not be edite"
  },
  {
    "path": "apps/nextjs-app/next.config.mjs",
    "chars": 118,
    "preview": "/** @type {import('next').NextConfig} */\nconst nextConfig = {\n  reactStrictMode: true,\n};\n\nexport default nextConfig;\n"
  },
  {
    "path": "apps/nextjs-app/package.json",
    "chars": 3728,
    "preview": "{\n  \"name\": \"bulletproof-react-nextjs-pages\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": "
  },
  {
    "path": "apps/nextjs-app/playwright.config.ts",
    "chars": 1613,
    "preview": "import { defineConfig, devices } from '@playwright/test';\n\nconst PORT = 3000;\n\n/**\n * Read environment variables from fi"
  },
  {
    "path": "apps/nextjs-app/plopfile.cjs",
    "chars": 216,
    "preview": "const componentGenerator = require('./generators/component/index');\n\n/**\n *\n * @param {import('plop').NodePlopAPI} plop\n"
  },
  {
    "path": "apps/nextjs-app/postcss.config.cjs",
    "chars": 83,
    "preview": "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n};\n"
  },
  {
    "path": "apps/nextjs-app/public/_redirects",
    "chars": 18,
    "preview": "/* /index.html 200"
  },
  {
    "path": "apps/nextjs-app/public/mockServiceWorker.js",
    "chars": 7514,
    "preview": "/* eslint-disable */\n/* tslint:disable */\n\n/**\n * Mock Service Worker.\n * @see https://github.com/mswjs/msw\n * - Please "
  },
  {
    "path": "apps/nextjs-app/public/robots.txt",
    "chars": 67,
    "preview": "# https://www.robotstxt.org/robotstxt.html\nUser-agent: *\nDisallow:\n"
  },
  {
    "path": "apps/nextjs-app/src/app/app/_components/dashboard-info.tsx",
    "chars": 934,
    "preview": "'use client';\n\nimport { useUser } from '@/lib/auth';\n\nexport const DashboardInfo = () => {\n  const user = useUser();\n\n  "
  },
  {
    "path": "apps/nextjs-app/src/app/app/_components/dashboard-layout.tsx",
    "chars": 6424,
    "preview": "'use client';\n\nimport { Home, PanelLeft, Folder, Users, User2 } from 'lucide-react';\nimport NextLink from 'next/link';\ni"
  },
  {
    "path": "apps/nextjs-app/src/app/app/discussions/[discussionId]/__tests__/discussion.test.tsx",
    "chars": 4100,
    "preview": "import { useParams } from 'next/navigation';\n\nimport {\n  renderApp,\n  screen,\n  userEvent,\n  waitFor,\n  createDiscussion"
  },
  {
    "path": "apps/nextjs-app/src/app/app/discussions/[discussionId]/_components/discussion.tsx",
    "chars": 906,
    "preview": "'use client';\n\nimport { ErrorBoundary } from 'react-error-boundary';\n\nimport { ContentLayout } from '@/components/layout"
  },
  {
    "path": "apps/nextjs-app/src/app/app/discussions/[discussionId]/page.tsx",
    "chars": 1650,
    "preview": "import {\n  dehydrate,\n  HydrationBoundary,\n  QueryClient,\n} from '@tanstack/react-query';\n\nimport { getInfiniteCommentsQ"
  },
  {
    "path": "apps/nextjs-app/src/app/app/discussions/__tests__/discussions.test.tsx",
    "chars": 2352,
    "preview": "import type { Mock } from 'vitest';\n\nimport { createDiscussion } from '@/testing/data-generators';\nimport {\n  renderApp,"
  },
  {
    "path": "apps/nextjs-app/src/app/app/discussions/_components/discussions.tsx",
    "chars": 978,
    "preview": "'use client';\n\nimport { useQueryClient } from '@tanstack/react-query';\n\nimport { ContentLayout } from '@/components/layo"
  },
  {
    "path": "apps/nextjs-app/src/app/app/discussions/page.tsx",
    "chars": 811,
    "preview": "import {\n  dehydrate,\n  HydrationBoundary,\n  QueryClient,\n} from '@tanstack/react-query';\n\nimport { getDiscussionsQueryO"
  },
  {
    "path": "apps/nextjs-app/src/app/app/layout.tsx",
    "chars": 332,
    "preview": "import { ReactNode } from 'react';\n\nimport { DashboardLayout } from './_components/dashboard-layout';\n\nexport const meta"
  },
  {
    "path": "apps/nextjs-app/src/app/app/page.tsx",
    "chars": 241,
    "preview": "import { DashboardInfo } from './_components/dashboard-info';\n\nexport const metadata = {\n  title: 'Dashboard',\n  descrip"
  },
  {
    "path": "apps/nextjs-app/src/app/app/profile/_components/profile.tsx",
    "chars": 1534,
    "preview": "'use client';\n\nimport { UpdateProfile } from '@/features/users/components/update-profile';\nimport { useUser } from '@/li"
  },
  {
    "path": "apps/nextjs-app/src/app/app/profile/page.tsx",
    "chars": 208,
    "preview": "import { Profile } from './_components/profile';\n\nexport const metadata = {\n  title: 'Profile',\n  description: 'Profile'"
  },
  {
    "path": "apps/nextjs-app/src/app/app/users/_components/admin-guard.tsx",
    "chars": 438,
    "preview": "'use client';\n\nimport { Spinner } from '@/components/ui/spinner';\nimport { useUser } from '@/lib/auth';\nimport { canView"
  },
  {
    "path": "apps/nextjs-app/src/app/app/users/_components/users.tsx",
    "chars": 531,
    "preview": "import {\n  dehydrate,\n  HydrationBoundary,\n  QueryClient,\n} from '@tanstack/react-query';\n\nimport { getUsersQueryOptions"
  },
  {
    "path": "apps/nextjs-app/src/app/app/users/page.tsx",
    "chars": 428,
    "preview": "import { ContentLayout } from '@/components/layouts/content-layout';\n\nimport { AdminGuard } from './_components/admin-gu"
  },
  {
    "path": "apps/nextjs-app/src/app/auth/_components/auth-layout.tsx",
    "chars": 1694,
    "preview": "'use client';\n\nimport { useRouter, usePathname, useSearchParams } from 'next/navigation';\nimport { ReactNode, useEffect "
  },
  {
    "path": "apps/nextjs-app/src/app/auth/layout.tsx",
    "chars": 778,
    "preview": "import { ReactNode, Suspense } from 'react';\nimport { ErrorBoundary } from 'react-error-boundary';\n\nimport { Spinner } f"
  },
  {
    "path": "apps/nextjs-app/src/app/auth/login/page.tsx",
    "chars": 569,
    "preview": "'use client';\n\nimport { useRouter, useSearchParams } from 'next/navigation';\n\nimport { paths } from '@/config/paths';\nim"
  },
  {
    "path": "apps/nextjs-app/src/app/auth/register/page.tsx",
    "chars": 947,
    "preview": "'use client';\n\nimport { useRouter, useSearchParams } from 'next/navigation';\nimport { useState } from 'react';\n\nimport {"
  },
  {
    "path": "apps/nextjs-app/src/app/layout.tsx",
    "chars": 1065,
    "preview": "import {\n  dehydrate,\n  HydrationBoundary,\n  QueryClient,\n} from '@tanstack/react-query';\nimport { ReactNode } from 'rea"
  },
  {
    "path": "apps/nextjs-app/src/app/not-found.tsx",
    "chars": 422,
    "preview": "import { Link } from '@/components/ui/link';\nimport { paths } from '@/config/paths';\n\nconst NotFoundPage = () => {\n  ret"
  },
  {
    "path": "apps/nextjs-app/src/app/page.tsx",
    "chars": 3311,
    "preview": "import { Button } from '@/components/ui/button';\nimport { Link } from '@/components/ui/link';\nimport { paths } from '@/c"
  },
  {
    "path": "apps/nextjs-app/src/app/provider.tsx",
    "chars": 944,
    "preview": "'use client';\n\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query';\nimport { ReactQueryDevtools } f"
  },
  {
    "path": "apps/nextjs-app/src/app/public/discussions/[discussionId]/page.tsx",
    "chars": 1421,
    "preview": "import {\n  dehydrate,\n  HydrationBoundary,\n  QueryClient,\n} from '@tanstack/react-query';\n\nimport { Discussion } from '@"
  },
  {
    "path": "apps/nextjs-app/src/components/errors/main.tsx",
    "chars": 467,
    "preview": "import { Button } from '../ui/button';\n\nexport const MainErrorFallback = () => {\n  return (\n    <div\n      className=\"fl"
  },
  {
    "path": "apps/nextjs-app/src/components/layouts/content-layout.tsx",
    "chars": 497,
    "preview": "import { ReactNode } from 'react';\n\ntype ContentLayoutProps = {\n  children: ReactNode;\n  title?: string;\n};\n\nexport cons"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/button/button.stories.tsx",
    "chars": 307,
    "preview": "import { Meta, StoryObj } from '@storybook/react';\n\nimport { Button } from './button';\n\nconst meta: Meta<typeof Button> "
  },
  {
    "path": "apps/nextjs-app/src/components/ui/button/button.tsx",
    "chars": 2225,
    "preview": "import { Slot } from '@radix-ui/react-slot';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport *"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/button/index.ts",
    "chars": 26,
    "preview": "export * from './button';\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/dialog/__tests__/dialog.test.tsx",
    "chars": 1753,
    "preview": "import * as React from 'react';\n\nimport { Button } from '@/components/ui/button';\nimport { useDisclosure } from '@/hooks"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/dialog/confirmation-dialog/__tests__/confirmation-dialog.test.tsx",
    "chars": 1176,
    "preview": "import { Button } from '@/components/ui/button';\nimport { rtlRender, screen, userEvent, waitFor } from '@/testing/test-u"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/dialog/confirmation-dialog/confirmation-dialog.stories.tsx",
    "chars": 757,
    "preview": "import { Meta, StoryObj } from '@storybook/react';\n\nimport { Button } from '@/components/ui/button';\n\nimport { Confirmat"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/dialog/confirmation-dialog/confirmation-dialog.tsx",
    "chars": 2147,
    "preview": "'use client';\n\nimport { CircleAlert, Info } from 'lucide-react';\nimport * as React from 'react';\nimport { useEffect } fr"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/dialog/confirmation-dialog/index.ts",
    "chars": 39,
    "preview": "export * from './confirmation-dialog';\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/dialog/dialog.stories.tsx",
    "chars": 1444,
    "preview": "import { Meta, StoryObj } from '@storybook/react';\nimport * as React from 'react';\n\nimport { Button } from '@/components"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/dialog/dialog.tsx",
    "chars": 3901,
    "preview": "'use client';\n\nimport * as DialogPrimitive from '@radix-ui/react-dialog';\nimport { Cross2Icon } from '@radix-ui/react-ic"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/dialog/index.ts",
    "chars": 65,
    "preview": "export * from './dialog';\nexport * from './confirmation-dialog';\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/drawer/__tests__/drawer.test.tsx",
    "chars": 1655,
    "preview": "import { Button } from '@/components/ui/button';\nimport { rtlRender, screen, userEvent, waitFor } from '@/testing/test-u"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/drawer/drawer.stories.tsx",
    "chars": 1536,
    "preview": "import { Meta, StoryObj } from '@storybook/react';\n\nimport { Button } from '@/components/ui/button';\nimport { useDisclos"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/drawer/drawer.tsx",
    "chars": 4381,
    "preview": "'use client';\n\nimport * as DrawerPrimitive from '@radix-ui/react-dialog';\nimport { Cross2Icon } from '@radix-ui/react-ic"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/drawer/index.ts",
    "chars": 26,
    "preview": "export * from './drawer';\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/dropdown/dropdown.stories.tsx",
    "chars": 3024,
    "preview": "import type { Meta } from '@storybook/react';\nimport React from 'react';\n\nimport { Button } from '@/components/ui/button"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/dropdown/dropdown.tsx",
    "chars": 7397,
    "preview": "'use client';\n\nimport * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';\nimport {\n  CheckIcon,\n  ChevronRi"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/dropdown/index.ts",
    "chars": 28,
    "preview": "export * from './dropdown';\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/form/__tests__/form.test.tsx",
    "chars": 1954,
    "preview": "import { SubmitHandler } from 'react-hook-form';\nimport { z } from 'zod';\n\nimport { Button } from '@/components/ui/butto"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/form/error.tsx",
    "chars": 325,
    "preview": "export type ErrorProps = {\n  errorMessage?: string | null;\n};\n\nexport const Error = ({ errorMessage }: ErrorProps) => {\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/form/field-wrapper.tsx",
    "chars": 668,
    "preview": "import * as React from 'react';\nimport { type FieldError } from 'react-hook-form';\n\nimport { Error } from './error';\nimp"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/form/form-drawer.tsx",
    "chars": 1492,
    "preview": "'use client';\n\nimport * as React from 'react';\n\nimport { useDisclosure } from '@/hooks/use-disclosure';\n\nimport { Button"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/form/form.stories.tsx",
    "chars": 2046,
    "preview": "import { Meta, StoryObj } from '@storybook/react';\nimport { z } from 'zod';\n\nimport { Button } from '../button';\n\nimport"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/form/form.tsx",
    "chars": 5070,
    "preview": "'use client';\n\nimport { zodResolver } from '@hookform/resolvers/zod';\nimport * as LabelPrimitive from '@radix-ui/react-l"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/form/index.ts",
    "chars": 185,
    "preview": "export * from './form';\nexport * from './input';\nexport * from './select';\nexport * from './textarea';\nexport * from './"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/form/input.tsx",
    "chars": 1194,
    "preview": "import * as React from 'react';\nimport { type UseFormRegisterReturn } from 'react-hook-form';\n\nimport { cn } from '@/uti"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/form/label.tsx",
    "chars": 733,
    "preview": "'use client';\n\nimport * as LabelPrimitive from '@radix-ui/react-label';\nimport { cva, type VariantProps } from 'class-va"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/form/select.tsx",
    "chars": 1168,
    "preview": "'use client';\n\nimport * as React from 'react';\nimport { UseFormRegisterReturn } from 'react-hook-form';\n\nimport { cn } f"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/form/switch.tsx",
    "chars": 1170,
    "preview": "'use client';\n\nimport * as SwitchPrimitives from '@radix-ui/react-switch';\nimport * as React from 'react';\n\nimport { cn "
  },
  {
    "path": "apps/nextjs-app/src/components/ui/form/textarea.tsx",
    "chars": 1118,
    "preview": "import * as React from 'react';\nimport { UseFormRegisterReturn } from 'react-hook-form';\n\nimport { cn } from '@/utils/cn"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/link/index.ts",
    "chars": 24,
    "preview": "export * from './link';\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/link/link.stories.tsx",
    "chars": 287,
    "preview": "import { Meta, StoryObj } from '@storybook/react';\n\nimport { Link } from './link';\n\nconst meta: Meta<typeof Link> = {\n  "
  },
  {
    "path": "apps/nextjs-app/src/components/ui/link/link.tsx",
    "chars": 473,
    "preview": "import NextLink, { LinkProps as NextLinkProps } from 'next/link';\n\nimport { cn } from '@/utils/cn';\n\nexport type LinkPro"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/md-preview/index.ts",
    "chars": 30,
    "preview": "export * from './md-preview';\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/md-preview/md-preview.stories.tsx",
    "chars": 306,
    "preview": "import { Meta, StoryObj } from '@storybook/react';\n\nimport { MDPreview } from './md-preview';\n\nconst meta: Meta<typeof M"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/md-preview/md-preview.tsx",
    "chars": 378,
    "preview": "import DOMPurify from 'isomorphic-dompurify';\nimport { parse } from 'marked';\n\nexport type MDPreviewProps = {\n  value: s"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/notifications/__tests__/notifications.test.ts",
    "chars": 740,
    "preview": "import { renderHook, act } from '@testing-library/react';\n\nimport { useNotifications, Notification } from '../notificati"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/notifications/index.ts",
    "chars": 72,
    "preview": "export * from './notifications';\nexport * from './notifications-store';\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/notifications/notification.stories.tsx",
    "chars": 1371,
    "preview": "import { Meta, StoryObj } from '@storybook/react';\n\nimport { Notification } from './notification';\n\nconst meta: Meta<typ"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/notifications/notification.tsx",
    "chars": 1889,
    "preview": "'use client';\n\nimport { Info, CircleAlert, CircleX, CircleCheck } from 'lucide-react';\n\nconst icons = {\n  info: <Info cl"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/notifications/notifications-store.ts",
    "chars": 823,
    "preview": "import { nanoid } from 'nanoid';\nimport { create } from 'zustand';\n\nexport type Notification = {\n  id: string;\n  type: '"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/notifications/notifications.tsx",
    "chars": 624,
    "preview": "'use client';\n\nimport { Notification } from './notification';\nimport { useNotifications } from './notifications-store';\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/spinner/index.ts",
    "chars": 27,
    "preview": "export * from './spinner';\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/spinner/spinner.stories.tsx",
    "chars": 281,
    "preview": "import { Meta, StoryObj } from '@storybook/react';\n\nimport { Spinner } from './spinner';\n\nconst meta: Meta<typeof Spinne"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/spinner/spinner.tsx",
    "chars": 964,
    "preview": "import { cn } from '@/utils/cn';\n\nconst sizes = {\n  sm: 'h-4 w-4',\n  md: 'h-8 w-8',\n  lg: 'h-16 w-16',\n  xl: 'h-24 w-24'"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/table/index.ts",
    "chars": 25,
    "preview": "export * from './table';\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/table/pagination.tsx",
    "chars": 4586,
    "preview": "import {\n  ChevronLeftIcon,\n  ChevronRightIcon,\n  DotsHorizontalIcon,\n} from '@radix-ui/react-icons';\nimport * as React "
  },
  {
    "path": "apps/nextjs-app/src/components/ui/table/table.stories.tsx",
    "chars": 1041,
    "preview": "import { Meta, StoryObj } from '@storybook/react';\n\nimport { Table } from './table';\n\nconst meta: Meta<typeof Table> = {"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/table/table.tsx",
    "chars": 4433,
    "preview": "import { ArchiveX } from 'lucide-react';\nimport * as React from 'react';\n\nimport { BaseEntity } from '@/types/api';\nimpo"
  },
  {
    "path": "apps/nextjs-app/src/config/env.ts",
    "chars": 1041,
    "preview": "import * as z from 'zod';\nimport 'dotenv/config';\n\nconst createEnv = () => {\n  const EnvSchema = z.object({\n    API_URL:"
  },
  {
    "path": "apps/nextjs-app/src/config/paths.ts",
    "chars": 917,
    "preview": "export const paths = {\n  home: {\n    getHref: () => '/',\n  },\n\n  auth: {\n    register: {\n      getHref: (redirectTo?: st"
  },
  {
    "path": "apps/nextjs-app/src/features/auth/components/__tests__/login-form.test.tsx",
    "chars": 722,
    "preview": "import {\n  createUser,\n  renderApp,\n  screen,\n  userEvent,\n  waitFor,\n} from '@/testing/test-utils';\n\nimport { LoginForm"
  },
  {
    "path": "apps/nextjs-app/src/features/auth/components/__tests__/register-form.test.tsx",
    "chars": 1079,
    "preview": "import { createUser } from '@/testing/data-generators';\nimport { renderApp, screen, userEvent, waitFor } from '@/testing"
  },
  {
    "path": "apps/nextjs-app/src/features/auth/components/login-form.tsx",
    "chars": 1765,
    "preview": "'use client';\n\nimport NextLink from 'next/link';\nimport { useSearchParams } from 'next/navigation';\n\nimport { Button } f"
  },
  {
    "path": "apps/nextjs-app/src/features/auth/components/register-form.tsx",
    "chars": 3602,
    "preview": "'use client';\n\nimport NextLink from 'next/link';\nimport { useSearchParams } from 'next/navigation';\nimport * as React fr"
  },
  {
    "path": "apps/nextjs-app/src/features/comments/api/create-comment.ts",
    "chars": 1245,
    "preview": "import { useMutation, useQueryClient } from '@tanstack/react-query';\nimport { z } from 'zod';\n\nimport { api } from '@/li"
  },
  {
    "path": "apps/nextjs-app/src/features/comments/api/delete-comment.ts",
    "chars": 946,
    "preview": "import { useMutation, useQueryClient } from '@tanstack/react-query';\n\nimport { api } from '@/lib/api-client';\nimport { M"
  },
  {
    "path": "apps/nextjs-app/src/features/comments/api/get-comments.ts",
    "chars": 1253,
    "preview": "import { infiniteQueryOptions, useInfiniteQuery } from '@tanstack/react-query';\n\nimport { api } from '@/lib/api-client';"
  },
  {
    "path": "apps/nextjs-app/src/features/comments/components/comments-list.tsx",
    "chars": 2777,
    "preview": "'use client';\n\nimport { ArchiveX } from 'lucide-react';\nimport { usePathname } from 'next/navigation';\n\nimport { Button "
  },
  {
    "path": "apps/nextjs-app/src/features/comments/components/comments.tsx",
    "chars": 666,
    "preview": "'use client';\n\nimport { usePathname } from 'next/navigation';\n\nimport { CommentsList } from './comments-list';\nimport { "
  },
  {
    "path": "apps/nextjs-app/src/features/comments/components/create-comment.tsx",
    "chars": 1846,
    "preview": "'use client';\n\nimport { Plus } from 'lucide-react';\n\nimport { Button } from '@/components/ui/button';\nimport { Form, For"
  },
  {
    "path": "apps/nextjs-app/src/features/comments/components/delete-comment.tsx",
    "chars": 1412,
    "preview": "'use client';\n\nimport { Trash } from 'lucide-react';\n\nimport { Button } from '@/components/ui/button';\nimport { Confirma"
  },
  {
    "path": "apps/nextjs-app/src/features/discussions/api/create-discussion.ts",
    "chars": 1246,
    "preview": "import { useMutation, useQueryClient } from '@tanstack/react-query';\nimport { z } from 'zod';\n\nimport { api } from '@/li"
  },
  {
    "path": "apps/nextjs-app/src/features/discussions/api/delete-discussion.ts",
    "chars": 928,
    "preview": "import { useMutation, useQueryClient } from '@tanstack/react-query';\n\nimport { api } from '@/lib/api-client';\nimport { M"
  },
  {
    "path": "apps/nextjs-app/src/features/discussions/api/get-discussion.ts",
    "chars": 878,
    "preview": "import { useQuery, queryOptions } from '@tanstack/react-query';\n\nimport { api } from '@/lib/api-client';\nimport { QueryC"
  },
  {
    "path": "apps/nextjs-app/src/features/discussions/api/get-discussions.ts",
    "chars": 921,
    "preview": "import { queryOptions, useQuery } from '@tanstack/react-query';\n\nimport { api } from '@/lib/api-client';\nimport { QueryC"
  },
  {
    "path": "apps/nextjs-app/src/features/discussions/api/update-discussion.ts",
    "chars": 1316,
    "preview": "import { useMutation, useQueryClient } from '@tanstack/react-query';\nimport { z } from 'zod';\n\nimport { api } from '@/li"
  },
  {
    "path": "apps/nextjs-app/src/features/discussions/components/create-discussion.tsx",
    "chars": 2676,
    "preview": "'use client';\n\nimport { Plus } from 'lucide-react';\n\nimport { Button } from '@/components/ui/button';\nimport {\n  Form,\n "
  },
  {
    "path": "apps/nextjs-app/src/features/discussions/components/delete-discussion.tsx",
    "chars": 1492,
    "preview": "'use client';\n\nimport { Trash } from 'lucide-react';\n\nimport { Button } from '@/components/ui/button';\nimport { Confirma"
  },
  {
    "path": "apps/nextjs-app/src/features/discussions/components/discussion-view.tsx",
    "chars": 2294,
    "preview": "'use client';\n\nimport { Link as LinkIcon } from 'lucide-react';\nimport { usePathname } from 'next/navigation';\n\nimport {"
  },
  {
    "path": "apps/nextjs-app/src/features/discussions/components/discussions-list.tsx",
    "chars": 2463,
    "preview": "'use client';\n\nimport { useQueryClient } from '@tanstack/react-query';\nimport { useSearchParams } from 'next/navigation'"
  },
  {
    "path": "apps/nextjs-app/src/features/discussions/components/update-discussion.tsx",
    "chars": 3047,
    "preview": "'use client';\n\nimport { Pen } from 'lucide-react';\n\nimport { Button } from '@/components/ui/button';\nimport {\n  Form,\n  "
  },
  {
    "path": "apps/nextjs-app/src/features/teams/api/get-teams.ts",
    "chars": 657,
    "preview": "import { queryOptions, useQuery } from '@tanstack/react-query';\n\nimport { api } from '@/lib/api-client';\nimport { QueryC"
  },
  {
    "path": "apps/nextjs-app/src/features/users/api/delete-user.ts",
    "chars": 891,
    "preview": "import { useMutation, useQueryClient } from '@tanstack/react-query';\n\nimport { api } from '@/lib/api-client';\nimport { M"
  },
  {
    "path": "apps/nextjs-app/src/features/users/api/get-users.ts",
    "chars": 644,
    "preview": "import { queryOptions, useQuery } from '@tanstack/react-query';\n\nimport { api } from '@/lib/api-client';\nimport { QueryC"
  },
  {
    "path": "apps/nextjs-app/src/features/users/api/update-profile.ts",
    "chars": 1096,
    "preview": "import { useMutation } from '@tanstack/react-query';\nimport { z } from 'zod';\n\nimport { api } from '@/lib/api-client';\ni"
  },
  {
    "path": "apps/nextjs-app/src/features/users/components/delete-user.tsx",
    "chars": 1205,
    "preview": "'use client';\n\nimport { Button } from '@/components/ui/button';\nimport { ConfirmationDialog } from '@/components/ui/dial"
  },
  {
    "path": "apps/nextjs-app/src/features/users/components/update-profile.tsx",
    "chars": 2413,
    "preview": "'use client';\n\nimport { Pen } from 'lucide-react';\n\nimport { Button } from '@/components/ui/button';\nimport { Form, Form"
  },
  {
    "path": "apps/nextjs-app/src/features/users/components/users-list.tsx",
    "chars": 1292,
    "preview": "'use client';\n\nimport { Spinner } from '@/components/ui/spinner';\nimport { Table } from '@/components/ui/table';\nimport "
  },
  {
    "path": "apps/nextjs-app/src/hooks/__tests__/use-disclosure.test.ts",
    "chars": 1177,
    "preview": "import { renderHook, act } from '@testing-library/react';\n\nimport { useDisclosure } from '../use-disclosure';\n\ntest('sho"
  },
  {
    "path": "apps/nextjs-app/src/hooks/use-disclosure.ts",
    "chars": 387,
    "preview": "import * as React from 'react';\n\nexport const useDisclosure = (initial = false) => {\n  const [isOpen, setIsOpen] = React"
  },
  {
    "path": "apps/nextjs-app/src/lib/__tests__/authorization.test.tsx",
    "chars": 2731,
    "preview": "import { Comment, User } from '@/types/api';\n\nimport {\n  canCreateDiscussion,\n  canDeleteDiscussion,\n  canUpdateDiscussi"
  },
  {
    "path": "apps/nextjs-app/src/lib/api-client.ts",
    "chars": 3254,
    "preview": "import { useNotifications } from '@/components/ui/notifications';\nimport { env } from '@/config/env';\n\ntype RequestOptio"
  },
  {
    "path": "apps/nextjs-app/src/lib/auth.tsx",
    "chars": 2744,
    "preview": "import {\n  queryOptions,\n  useMutation,\n  useQuery,\n  useQueryClient,\n} from '@tanstack/react-query';\nimport { z } from "
  },
  {
    "path": "apps/nextjs-app/src/lib/authorization.ts",
    "chars": 727,
    "preview": "import { Comment, User } from '@/types/api';\n\nexport const canCreateDiscussion = (user: User | null | undefined) => {\n  "
  },
  {
    "path": "apps/nextjs-app/src/lib/react-query.ts",
    "chars": 667,
    "preview": "import { UseMutationOptions, DefaultOptions } from '@tanstack/react-query';\n\nexport const queryConfig = {\n  queries: {\n "
  },
  {
    "path": "apps/nextjs-app/src/styles/globals.css",
    "chars": 1944,
    "preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer base {\n  :root {\n    --background: 0 0% 100%;\n    --f"
  },
  {
    "path": "apps/nextjs-app/src/testing/data-generators.ts",
    "chars": 1643,
    "preview": "import {\n  randCompanyName,\n  randUserName,\n  randEmail,\n  randParagraph,\n  randUuid,\n  randPassword,\n  randCatchPhrase,"
  },
  {
    "path": "apps/nextjs-app/src/testing/mocks/browser.ts",
    "chars": 132,
    "preview": "import { setupWorker } from 'msw/browser';\n\nimport { handlers } from './handlers';\n\nexport const worker = setupWorker(.."
  },
  {
    "path": "apps/nextjs-app/src/testing/mocks/db.ts",
    "chars": 2503,
    "preview": "import { factory, primaryKey } from '@mswjs/data';\nimport { nanoid } from 'nanoid';\n\nconst models = {\n  user: {\n    id: "
  },
  {
    "path": "apps/nextjs-app/src/testing/mocks/handlers/auth.ts",
    "chars": 3952,
    "preview": "import Cookies from 'js-cookie';\nimport { HttpResponse, http } from 'msw';\n\nimport { env } from '@/config/env';\n\nimport "
  },
  {
    "path": "apps/nextjs-app/src/testing/mocks/handlers/comments.ts",
    "chars": 3546,
    "preview": "import { HttpResponse, http } from 'msw';\n\nimport { env } from '@/config/env';\n\nimport { db, persistDb } from '../db';\ni"
  },
  {
    "path": "apps/nextjs-app/src/testing/mocks/handlers/discussions.ts",
    "chars": 6053,
    "preview": "import { HttpResponse, http } from 'msw';\n\nimport { env } from '@/config/env';\n\nimport { db, persistDb } from '../db';\ni"
  },
  {
    "path": "apps/nextjs-app/src/testing/mocks/handlers/index.ts",
    "chars": 612,
    "preview": "import { HttpResponse, http } from 'msw';\n\nimport { env } from '@/config/env';\n\nimport { networkDelay } from '../utils';"
  },
  {
    "path": "apps/nextjs-app/src/testing/mocks/handlers/teams.ts",
    "chars": 519,
    "preview": "import { HttpResponse, http } from 'msw';\n\nimport { env } from '@/config/env';\n\nimport { db } from '../db';\nimport { net"
  },
  {
    "path": "apps/nextjs-app/src/testing/mocks/handlers/users.ts",
    "chars": 2469,
    "preview": "import { HttpResponse, http } from 'msw';\n\nimport { env } from '@/config/env';\n\nimport { db, persistDb } from '../db';\ni"
  },
  {
    "path": "apps/nextjs-app/src/testing/mocks/index.ts",
    "chars": 273,
    "preview": "import { env } from '@/config/env';\n\nexport const enableMocking = async () => {\n  if (env.ENABLE_API_MOCKING) {\n    cons"
  },
  {
    "path": "apps/nextjs-app/src/testing/mocks/server.ts",
    "chars": 129,
    "preview": "import { setupServer } from 'msw/node';\n\nimport { handlers } from './handlers';\n\nexport const server = setupServer(...ha"
  },
  {
    "path": "apps/nextjs-app/src/testing/mocks/utils.ts",
    "chars": 2446,
    "preview": "import Cookies from 'js-cookie';\nimport { delay } from 'msw';\n\nimport { db } from './db';\n\nexport const encode = (obj: a"
  },
  {
    "path": "apps/nextjs-app/src/testing/setup-tests.ts",
    "chars": 1080,
    "preview": "import '@testing-library/jest-dom/vitest';\n\nimport { initializeDb, resetDb } from '@/testing/mocks/db';\nimport { server "
  },
  {
    "path": "apps/nextjs-app/src/testing/test-utils.tsx",
    "chars": 1947,
    "preview": "import {\n  render as rtlRender,\n  waitForElementToBeRemoved,\n  screen,\n} from '@testing-library/react';\nimport userEvent"
  },
  {
    "path": "apps/nextjs-app/src/types/api.ts",
    "chars": 892,
    "preview": "// let's imagine this file is autogenerated from the backend\n// ideally, we want to keep these api related types in sync"
  },
  {
    "path": "apps/nextjs-app/src/utils/auth.ts",
    "chars": 453,
    "preview": "import { cookies } from 'next/headers';\n\nexport const AUTH_TOKEN_COOKIE_NAME = 'bulletproof_react_app_token';\n\nexport co"
  },
  {
    "path": "apps/nextjs-app/src/utils/cn.ts",
    "chars": 169,
    "preview": "import { type ClassValue, clsx } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: C"
  },
  {
    "path": "apps/nextjs-app/src/utils/format.ts",
    "chars": 132,
    "preview": "import { default as dayjs } from 'dayjs';\n\nexport const formatDate = (date: number) =>\n  dayjs(date).format('MMMM D, YYY"
  },
  {
    "path": "apps/nextjs-app/tailwind.config.cjs",
    "chars": 2219,
    "preview": "/** @type {import('tailwindcss').Config} */\n\nconst defaultTheme = require('tailwindcss/defaultTheme');\n\nmodule.exports ="
  },
  {
    "path": "apps/nextjs-app/tsconfig.json",
    "chars": 599,
    "preview": "{\n  \"compilerOptions\": {\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n  "
  },
  {
    "path": "apps/nextjs-app/vitest.config.ts",
    "chars": 475,
    "preview": "/// <reference types=\"vitest\" />\n\nimport react from '@vitejs/plugin-react';\nimport viteTsconfigPaths from 'vite-tsconfig"
  },
  {
    "path": "apps/nextjs-pages/.eslintrc.cjs",
    "chars": 4586,
    "preview": "module.exports = {\n  root: true,\n  env: {\n    node: true,\n    es6: true,\n  },\n  parserOptions: { ecmaVersion: 'latest', "
  },
  {
    "path": "apps/nextjs-pages/.gitignore",
    "chars": 524,
    "preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
  },
  {
    "path": "apps/nextjs-pages/.prettierignore",
    "chars": 5,
    "preview": "*.hbs"
  },
  {
    "path": "apps/nextjs-pages/.prettierrc",
    "chars": 109,
    "preview": "{\n  \"singleQuote\": true,\n  \"trailingComma\": \"all\",\n  \"printWidth\": 80,\n  \"tabWidth\": 2,\n  \"useTabs\": false\n}\n"
  },
  {
    "path": "apps/nextjs-pages/.storybook/main.ts",
    "chars": 477,
    "preview": "module.exports = {\n  stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],\n\n  addons: [\n    '@st"
  },
  {
    "path": "apps/nextjs-pages/.storybook/preview.tsx",
    "chars": 190,
    "preview": "import React from 'react';\nimport '../src/styles/globals.css';\n\nexport const parameters = {\n  actions: { argTypesRegex: "
  },
  {
    "path": "apps/nextjs-pages/.vscode/extensions.json",
    "chars": 200,
    "preview": "{\n  \"recommendations\": [\n    \"dbaeumer.vscode-eslint\",\n    \"esbenp.prettier-vscode\",\n    \"dsznajder.es7-react-js-snippet"
  },
  {
    "path": "apps/nextjs-pages/.vscode/settings.json",
    "chars": 110,
    "preview": "{\n  \"editor.formatOnSave\": true,\n  \"editor.codeActionsOnSave\": {\n    \"source.fixAll.eslint\": \"explicit\"\n  }\n}\n"
  },
  {
    "path": "apps/nextjs-pages/README.md",
    "chars": 957,
    "preview": "# Next.js Pages Application\n\n## Get Started\n\nPrerequisites:\n\n- Node 20+\n- Yarn 1.22+\n\nTo set up the app execute the foll"
  },
  {
    "path": "apps/nextjs-pages/__mocks__/vitest-env.d.ts",
    "chars": 79,
    "preview": "/// <reference types=\"vite/client\" />\n/// <reference types=\"vitest/globals\" />\n"
  },
  {
    "path": "apps/nextjs-pages/__mocks__/zustand.ts",
    "chars": 1754,
    "preview": "import { act } from '@testing-library/react';\nimport { afterEach, vi } from 'vitest';\nimport * as zustand from 'zustand'"
  },
  {
    "path": "apps/nextjs-pages/e2e/.eslintrc.cjs",
    "chars": 119,
    "preview": "module.exports = {\n  root: true,\n  parser: '@typescript-eslint/parser',\n  extends: 'plugin:playwright/recommended',\n};\n"
  },
  {
    "path": "apps/nextjs-pages/e2e/tests/auth.setup.ts",
    "chars": 1665,
    "preview": "import { test as setup, expect } from '@playwright/test';\nimport { createUser } from '../../src/testing/data-generators'"
  },
  {
    "path": "apps/nextjs-pages/e2e/tests/profile.spec.ts",
    "chars": 658,
    "preview": "import { test, expect } from '@playwright/test';\n\ntest('profile', async ({ page }) => {\n  // update user:\n  await page.g"
  },
  {
    "path": "apps/nextjs-pages/e2e/tests/smoke.spec.ts",
    "chars": 3340,
    "preview": "import { test, expect } from '@playwright/test';\n\nimport {\n  createDiscussion,\n  createComment,\n} from '../../src/testin"
  },
  {
    "path": "apps/nextjs-pages/generators/component/component.stories.tsx.hbs",
    "chars": 330,
    "preview": "import { Meta, StoryObj } from '@storybook/react';\n\nimport { {{ properCase name }} } from './{{ kebabCase name }}';\n\ncon"
  },
  {
    "path": "apps/nextjs-pages/generators/component/component.tsx.hbs",
    "chars": 219,
    "preview": "import * as React from \"react\"; \n\nexport type {{properCase name}}Props = {};\n\nexport const {{properCase name}} = (props:"
  },
  {
    "path": "apps/nextjs-pages/generators/component/index.cjs",
    "chars": 1532,
    "preview": "const path = require('path');\nconst fs = require('fs');\n\nconst featuresDir = path.join(process.cwd(), 'src/features');\nc"
  },
  {
    "path": "apps/nextjs-pages/generators/component/index.ts.hbs",
    "chars": 40,
    "preview": "export * from './{{ kebabCase name }}';\n"
  },
  {
    "path": "apps/nextjs-pages/lint-staged.config.mjs",
    "chars": 345,
    "preview": "import path from 'path';\n\nconst buildEslintCommand = (filenames) => {\n  return `next lint --fix --file ${filenames\n    ."
  },
  {
    "path": "apps/nextjs-pages/mock-server.ts",
    "chars": 735,
    "preview": "import { createMiddleware } from '@mswjs/http-middleware';\nimport cors from 'cors';\nimport express from 'express';\nimpor"
  },
  {
    "path": "apps/nextjs-pages/next-env.d.ts",
    "chars": 267,
    "preview": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\n/// <reference types=\"next/navigation-t"
  },
  {
    "path": "apps/nextjs-pages/next.config.mjs",
    "chars": 118,
    "preview": "/** @type {import('next').NextConfig} */\nconst nextConfig = {\n  reactStrictMode: true,\n};\n\nexport default nextConfig;\n"
  },
  {
    "path": "apps/nextjs-pages/package.json",
    "chars": 3820,
    "preview": "{\n  \"name\": \"bulletproof-react-nextjs-pages\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": "
  },
  {
    "path": "apps/nextjs-pages/playwright.config.ts",
    "chars": 1613,
    "preview": "import { defineConfig, devices } from '@playwright/test';\n\nconst PORT = 3000;\n\n/**\n * Read environment variables from fi"
  },
  {
    "path": "apps/nextjs-pages/plopfile.cjs",
    "chars": 216,
    "preview": "const componentGenerator = require('./generators/component/index');\n\n/**\n *\n * @param {import('plop').NodePlopAPI} plop\n"
  },
  {
    "path": "apps/nextjs-pages/postcss.config.cjs",
    "chars": 83,
    "preview": "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n};\n"
  },
  {
    "path": "apps/nextjs-pages/public/_redirects",
    "chars": 18,
    "preview": "/* /index.html 200"
  }
]

// ... and 311 more files (download for full content)

About this extraction

This page contains the full source code of the alan2207/bulletproof-react GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 511 files (699.2 KB), approximately 192.7k tokens, and a symbol index with 342 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!