[
  {
    "path": ".github/workflows/nextjs-app-ci.yml",
    "content": "name: Next.js App CI\non:\n  push:\n    branches: [\"*\"]\n    paths-ignore:\n      - \"README.md\"\n      - \"docs/**\"\n  pull_request:\n    branches: [master]\n    paths-ignore:\n      - \"README.md\"\n      - \"docs/**\"\njobs:\n  all-cli-checks:\n    runs-on: ubuntu-latest\n    defaults:\n      run:\n        working-directory: ./apps/nextjs-app\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: lts/*\n      - name: Set environment variables\n        run: mv .env.example .env\n      - name: Install dependencies\n        run: yarn install\n      - name: Build application\n        run: yarn build\n      - name: Run tests\n        run: yarn test\n      - name: Run linter\n        run: yarn lint\n      - name: Check types\n        run: yarn check-types\n  e2e:\n    timeout-minutes: 60\n    runs-on: ubuntu-latest\n    defaults:\n      run:\n        working-directory: ./apps/nextjs-app\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: lts/*\n      - name: Set environment variables\n        run: mv .env.example-e2e .env\n      - name: Install dependencies\n        run: npm install -g yarn && yarn\n      - name: Install Playwright Browsers\n        run: yarn playwright install --with-deps\n      - name: Run Playwright tests\n        run: yarn test-e2e\n      - uses: actions/upload-artifact@v4\n        if: always()\n        with:\n          name: playwright-report\n          path: |\n            playwright-report/\n            mocked-db.json\n          retention-days: 30\n"
  },
  {
    "path": ".github/workflows/nextjs-pages-ci.yml",
    "content": "name: Next.js Pages CI\non:\n  push:\n    branches: [\"*\"]\n    paths-ignore:\n      - \"README.md\"\n      - \"docs/**\"\n  pull_request:\n    branches: [master]\n    paths-ignore:\n      - \"README.md\"\n      - \"docs/**\"\njobs:\n  all-cli-checks:\n    runs-on: ubuntu-latest\n    defaults:\n      run:\n        working-directory: ./apps/nextjs-pages\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: lts/*\n      - name: Set environment variables\n        run: mv .env.example .env\n      - name: Install dependencies\n        run: yarn install\n      - name: Build application\n        run: yarn build\n      - name: Run tests\n        run: yarn test\n      - name: Run linter\n        run: yarn lint\n      - name: Check types\n        run: yarn check-types\n  e2e:\n    timeout-minutes: 60\n    runs-on: ubuntu-latest\n    defaults:\n      run:\n        working-directory: ./apps/nextjs-pages\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: lts/*\n      - name: Set environment variables\n        run: mv .env.example-e2e .env\n      - name: Install dependencies\n        run: npm install -g yarn && yarn\n      - name: Install Playwright Browsers\n        run: yarn playwright install --with-deps\n      - name: Run Playwright tests\n        run: yarn test-e2e\n      - uses: actions/upload-artifact@v4\n        if: always()\n        with:\n          name: playwright-report\n          path: |\n            playwright-report/\n            mocked-db.json\n          retention-days: 30\n"
  },
  {
    "path": ".github/workflows/react-vite-ci.yml",
    "content": "name: React Vite CI\non:\n  push:\n    branches: [\"*\"]\n    paths-ignore:\n      - \"README.md\"\n      - \"docs/**\"\n  pull_request:\n    branches: [master]\n    paths-ignore:\n      - \"README.md\"\n      - \"docs/**\"\njobs:\n  all-cli-checks:\n    runs-on: ubuntu-latest\n    defaults:\n      run:\n        working-directory: ./apps/react-vite\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: lts/*\n      - name: Set environment variables\n        run: mv .env.example .env\n      - name: Install dependencies\n        run: yarn install\n      - name: Build application\n        run: yarn build\n      - name: Run tests\n        run: yarn test\n      - name: Run linter\n        run: yarn lint\n      - name: Check types\n        run: yarn check-types\n  e2e:\n    timeout-minutes: 60\n    runs-on: ubuntu-latest\n    defaults:\n      run:\n        working-directory: ./apps/react-vite\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: lts/*\n      - name: Set environment variables\n        run: mv .env.example-e2e .env\n      - name: Install dependencies\n        run: npm install -g yarn && yarn\n      - name: Install Playwright Browsers\n        run: yarn playwright install --with-deps\n      - name: Run Playwright tests\n        run: yarn test-e2e\n      - uses: actions/upload-artifact@v4\n        if: always()\n        with:\n          name: playwright-report\n          path: |\n            playwright-report/\n            mocked-db.json\n          retention-days: 30\n"
  },
  {
    "path": ".gitignore",
    "content": "# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# misc\n.DS_Store"
  },
  {
    "path": ".husky/pre-commit",
    "content": "yarn --cwd apps/nextjs-app lint-staged && yarn --cwd apps/nextjs-pages lint-staged && yarn --cwd apps/react-vite lint-staged"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2024 Alan Alickovic\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Bulletproof React 🛡️ ⚛️\n\n[![MIT License](https://img.shields.io/github/license/alan2207/bulletproof-react)](https://github.com/alan2207/bulletproof-react/blob/master/LICENSE)\n[![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)\n[![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)\n[![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)\n\nA simple, scalable, and powerful architecture for building production ready React applications.\n\n## Introduction\n\nReact 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.\n\nThis 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.\n\nThe 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.\n\nFeel free to explore the sample app codebase to get the most value out of the repo.\n\n> 🤝 **Looking for help implementing these patterns at your company?** [Get in touch](mailto:alan2207@live.com)\n\n## What makes a React application \"bulletproof\"?\n\nThis 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:\n\n- Easy to get started with\n- Simple to understand and maintain\n- Uses the right tools for the job\n- Clean boundaries between different parts of the application\n- Everyone on the team is on the same page when it comes to how things are done\n- Secure\n- Performant\n- Scalable in terms of codebase and team size\n- Issues detectable as early as possible\n\n#### Disclaimer:\n\nThis 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.\n\nTo 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.\n\n## Table Of Contents:\n\n- [💻 Application Overview](docs/application-overview.md)\n- [⚙️ Project Standards](docs/project-standards.md)\n- [🗄️ Project Structure](docs/project-structure.md)\n- [🧱 Components And Styling](docs/components-and-styling.md)\n- [📡 API Layer](docs/api-layer.md)\n- [🗃️ State Management](docs/state-management.md)\n- [🧪 Testing](docs/testing.md)\n- [⚠️ Error Handling](docs/error-handling.md)\n- [🔐 Security](docs/security.md)\n- [🚄 Performance](docs/performance.md)\n- [🌐 Deployment](docs/deployment.md)\n- [📚 Additional Resources](docs/additional-resources.md)\n\n## Contributing\n\nContributions are always welcome! If you have any ideas, suggestions, fixes, feel free to contribute. You can do that by going through the following steps:\n\n1. Clone this repo\n2. Create a branch: `git checkout -b your-feature`\n3. Execute the `yarn prepare` script.\n4. Make some changes\n5. Test your changes\n6. Push your branch and open a Pull Request\n\n## License\n\n[MIT](/LICENSE)\n"
  },
  {
    "path": "apps/nextjs-app/.eslintrc.cjs",
    "content": "module.exports = {\n  root: true,\n  env: {\n    node: true,\n    es6: true,\n  },\n  parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },\n  ignorePatterns: [\n    'node_modules/*',\n    'public/mockServiceWorker.js',\n    'generators/*',\n  ],\n  extends: ['eslint:recommended', 'next/core-web-vitals'],\n  overrides: [\n    {\n      files: ['**/*.ts', '**/*.tsx'],\n      parser: '@typescript-eslint/parser',\n      settings: {\n        react: { version: 'detect' },\n        'import/resolver': {\n          typescript: {},\n        },\n      },\n      env: {\n        browser: true,\n        node: true,\n        es6: true,\n      },\n      extends: [\n        'eslint:recommended',\n        'plugin:import/errors',\n        'plugin:import/warnings',\n        'plugin:import/typescript',\n        'plugin:@typescript-eslint/recommended',\n        'plugin:react/recommended',\n        'plugin:react-hooks/recommended',\n        'plugin:jsx-a11y/recommended',\n        'plugin:prettier/recommended',\n        'plugin:testing-library/react',\n        'plugin:jest-dom/recommended',\n        'plugin:tailwindcss/recommended',\n        'plugin:vitest/legacy-recommended',\n      ],\n      rules: {\n        '@next/next/no-img-element': 'off',\n        'import/no-restricted-paths': [\n          'error',\n          {\n            zones: [\n              // disables cross-feature imports:\n              // eg. src/features/discussions should not import from src/features/comments, etc.\n              {\n                target: './src/features/auth',\n                from: './src/features',\n                except: ['./auth'],\n              },\n              {\n                target: './src/features/comments',\n                from: './src/features',\n                except: ['./comments'],\n              },\n              {\n                target: './src/features/discussions',\n                from: './src/features',\n                except: ['./discussions'],\n              },\n              {\n                target: './src/features/teams',\n                from: './src/features',\n                except: ['./teams'],\n              },\n              {\n                target: './src/features/users',\n                from: './src/features',\n                except: ['./users'],\n              },\n              // enforce unidirectional codebase:\n\n              // e.g. src/app can import from src/features but not the other way around\n              {\n                target: './src/features',\n                from: './src/app',\n              },\n\n              // e.g src/features and src/app can import from these shared modules but not the other way around\n              {\n                target: [\n                  './src/components',\n                  './src/hooks',\n                  './src/lib',\n                  './src/types',\n                  './src/utils',\n                ],\n                from: ['./src/features', './src/app'],\n              },\n            ],\n          },\n        ],\n        'import/no-cycle': 'error',\n        'linebreak-style': ['error', 'unix'],\n        'react/prop-types': 'off',\n        'import/order': [\n          'error',\n          {\n            groups: [\n              'builtin',\n              'external',\n              'internal',\n              'parent',\n              'sibling',\n              'index',\n              'object',\n            ],\n            'newlines-between': 'always',\n            alphabetize: { order: 'asc', caseInsensitive: true },\n          },\n        ],\n        'import/default': 'off',\n        'import/no-named-as-default-member': 'off',\n        'import/no-named-as-default': 'off',\n        'react/react-in-jsx-scope': 'off',\n        'jsx-a11y/anchor-is-valid': 'off',\n        '@typescript-eslint/no-unused-vars': ['error'],\n        '@typescript-eslint/explicit-function-return-type': ['off'],\n        '@typescript-eslint/explicit-module-boundary-types': ['off'],\n        '@typescript-eslint/no-empty-function': ['off'],\n        '@typescript-eslint/no-explicit-any': ['off'],\n        'prettier/prettier': ['error', {}, { usePrettierrc: true }],\n      },\n    },\n    {\n      plugins: ['check-file'],\n      files: ['src/**/*'],\n      rules: {\n        'check-file/filename-naming-convention': [\n          'error',\n          {\n            '**/*.{ts,tsx}': 'KEBAB_CASE',\n          },\n          {\n            ignoreMiddleExtensions: true,\n          },\n        ],\n        'check-file/folder-naming-convention': [\n          'error',\n          {\n            '!(src/app)/**/*': 'KEBAB_CASE',\n            '!(**/__tests__)/**/*': 'KEBAB_CASE',\n          },\n        ],\n      },\n    },\n  ],\n};\n"
  },
  {
    "path": "apps/nextjs-app/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n/test-results/\n/playwright-report/\n/blob-report/\n/playwright/.cache/\n/e2e/.auth/\n\n# storybook\nmigration-storybook.log\nstorybook.log\nstorybook-static\n\n\n# production\n/dist\n\n# misc\n.DS_Store\n.env\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n\n# local\nmocked-db.json\n\n/.next\n/.vite\ntsconfig.tsbuildinfo"
  },
  {
    "path": "apps/nextjs-app/.prettierignore",
    "content": "*.hbs"
  },
  {
    "path": "apps/nextjs-app/.prettierrc",
    "content": "{\n  \"singleQuote\": true,\n  \"trailingComma\": \"all\",\n  \"printWidth\": 80,\n  \"tabWidth\": 2,\n  \"useTabs\": false\n}\n"
  },
  {
    "path": "apps/nextjs-app/.storybook/main.ts",
    "content": "module.exports = {\n  stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],\n\n  addons: [\n    '@storybook/addon-actions',\n    '@storybook/addon-links',\n    '@storybook/node-logger',\n    '@storybook/addon-essentials',\n    '@storybook/addon-interactions',\n    '@storybook/addon-docs',\n    '@storybook/addon-a11y',\n  ],\n  framework: '@storybook/nextjs',\n  docs: {\n    autodocs: 'tag',\n  },\n  typescript: {\n    reactDocgen: 'react-docgen-typescript',\n  },\n};\n"
  },
  {
    "path": "apps/nextjs-app/.storybook/preview.tsx",
    "content": "import React from 'react';\nimport '../src/styles/globals.css';\n\nexport const parameters = {\n  actions: { argTypesRegex: '^on[A-Z].*' },\n};\n\nexport const decorators = [(Story) => <Story />];\n"
  },
  {
    "path": "apps/nextjs-app/.vscode/extensions.json",
    "content": "{\n  \"recommendations\": [\n    \"dbaeumer.vscode-eslint\",\n    \"esbenp.prettier-vscode\",\n    \"dsznajder.es7-react-js-snippets\",\n    \"mariusalchimavicius.json-to-ts\",\n    \"bradlc.vscode-tailwindcss\"\n  ]\n}\n"
  },
  {
    "path": "apps/nextjs-app/.vscode/settings.json",
    "content": "{\n  \"editor.formatOnSave\": true,\n  \"editor.codeActionsOnSave\": {\n    \"source.fixAll.eslint\": \"explicit\"\n  }\n}\n"
  },
  {
    "path": "apps/nextjs-app/README.md",
    "content": "# 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 following commands.\n\n```bash\ngit clone https://github.com/alan2207/bulletproof-react.git\ncd bulletproof-react\ncd apps/nextjs-app\ncp .env.example .env\nyarn install\n```\n\n#### `yarn run-mock-server`\n\nMake sure to start the mock server before running the app.\nThe mock server runs on [http://localhost:8080/api](http://localhost:8080/api).\n\n##### `yarn dev`\n\nRuns the app in the development mode.\\\nOpen [http://localhost:3000](http://localhost:3000) to view it in the browser.\n"
  },
  {
    "path": "apps/nextjs-app/__mocks__/vitest-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n/// <reference types=\"vitest/globals\" />\n"
  },
  {
    "path": "apps/nextjs-app/__mocks__/zustand.ts",
    "content": "import { act } from '@testing-library/react';\nimport { afterEach, vi } from 'vitest';\nimport * as zustand from 'zustand';\n\nconst { create: actualCreate, createStore: actualCreateStore } =\n  await vi.importActual<typeof zustand>('zustand');\n\n// a variable to hold reset functions for all stores declared in the app\nexport const storeResetFns = new Set<() => void>();\n\nconst createUncurried = <T>(stateCreator: zustand.StateCreator<T>) => {\n  const store = actualCreate(stateCreator);\n  const initialState = store.getInitialState();\n  storeResetFns.add(() => {\n    store.setState(initialState, true);\n  });\n  return store;\n};\n\n// when creating a store, we get its initial state, create a reset function and add it in the set\nexport const create = (<T>(stateCreator: zustand.StateCreator<T>) => {\n  // to support curried version of create\n  return typeof stateCreator === 'function'\n    ? createUncurried(stateCreator)\n    : createUncurried;\n}) as typeof zustand.create;\n\nconst createStoreUncurried = <T>(stateCreator: zustand.StateCreator<T>) => {\n  const store = actualCreateStore(stateCreator);\n  const initialState = store.getInitialState();\n  storeResetFns.add(() => {\n    store.setState(initialState, true);\n  });\n  return store;\n};\n\n// when creating a store, we get its initial state, create a reset function and add it in the set\nexport const createStore = (<T>(stateCreator: zustand.StateCreator<T>) => {\n  // to support curried version of createStore\n  return typeof stateCreator === 'function'\n    ? createStoreUncurried(stateCreator)\n    : createStoreUncurried;\n}) as typeof zustand.createStore;\n\n// reset all stores after each test run\nafterEach(() => {\n  act(() => {\n    storeResetFns.forEach((resetFn) => {\n      resetFn();\n    });\n  });\n});\n"
  },
  {
    "path": "apps/nextjs-app/e2e/.eslintrc.cjs",
    "content": "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",
    "content": "import { test as setup, expect } from '@playwright/test';\nimport { createUser } from '../../src/testing/data-generators';\n\nconst authFile = 'e2e/.auth/user.json';\n\nsetup('authenticate', async ({ page }) => {\n  const user = createUser();\n\n  await page.goto('/');\n  await page.getByRole('button', { name: 'Get started' }).click();\n  await page.waitForURL('/auth/login');\n  await page.getByRole('link', { name: 'Register' }).click();\n\n  // registration:\n  await page.getByLabel('First Name').click();\n  await page.getByLabel('First Name').fill(user.firstName);\n  await page.getByLabel('Last Name').click();\n  await page.getByLabel('Last Name').fill(user.lastName);\n  await page.getByLabel('Email Address').click();\n  await page.getByLabel('Email Address').fill(user.email);\n  await page.getByLabel('Password').click();\n  await page.getByLabel('Password').fill(user.password);\n  await page.getByLabel('Team Name').click();\n  await page.getByLabel('Team Name').fill(user.teamName);\n  await page.getByRole('button', { name: 'Register' }).click();\n  await page.waitForURL('/app');\n\n  // log out:\n  await page.getByRole('button', { name: 'Open user menu' }).click();\n  await page.getByRole('menuitem', { name: 'Sign Out' }).click();\n  await page.waitForURL('/auth/login?redirectTo=%2Fapp');\n\n  // log in:\n  await page.getByLabel('Email Address').click();\n  await page.getByLabel('Email Address').fill(user.email);\n  await page.getByLabel('Password').click();\n  await page.getByLabel('Password').fill(user.password);\n  await page.getByRole('button', { name: 'Log in' }).click();\n  await page.waitForURL('/app');\n\n  await page.context().storageState({ path: authFile });\n});\n"
  },
  {
    "path": "apps/nextjs-app/e2e/tests/profile.spec.ts",
    "content": "import { test, expect } from '@playwright/test';\n\ntest('profile', async ({ page }) => {\n  // update user:\n  await page.goto('/app');\n  await page.getByRole('button', { name: 'Open user menu' }).click();\n  await page.getByRole('menuitem', { name: 'Your Profile' }).click();\n  await page.getByRole('button', { name: 'Update Profile' }).click();\n  await page.getByLabel('Bio').click();\n  await page.getByLabel('Bio').fill('My bio');\n  await page.getByRole('button', { name: 'Submit' }).click();\n  await page\n    .getByLabel('Profile Updated')\n    .getByRole('button', { name: 'Close' })\n    .click();\n  await expect(page.getByText('My bio')).toBeVisible();\n});\n"
  },
  {
    "path": "apps/nextjs-app/e2e/tests/smoke.spec.ts",
    "content": "import { test, expect } from '@playwright/test';\n\nimport {\n  createDiscussion,\n  createComment,\n} from '../../src/testing/data-generators';\ntest('smoke', async ({ page }) => {\n  const discussion = createDiscussion();\n  const comment = createComment();\n\n  await page.goto('/');\n  await page.getByRole('button', { name: 'Get started' }).click();\n  await page.waitForURL('/app');\n\n  // create discussion:\n  await page.getByRole('link', { name: 'Discussions' }).click();\n  await page.waitForURL('/app/discussions');\n\n  await page.getByRole('button', { name: 'Create Discussion' }).click();\n  await page.getByLabel('Title').click();\n  await page.getByLabel('Title').fill(discussion.title);\n  await page.getByLabel('Body').click();\n  await page.getByLabel('Body').fill(discussion.body);\n  await page.getByRole('button', { name: 'Submit' }).click();\n  await page\n    .getByLabel('Discussion Created')\n    .getByRole('button', { name: 'Close' })\n    .click();\n\n  // visit discussion page:\n  await page.getByRole('link', { name: 'View' }).click();\n\n  await expect(\n    page.getByRole('heading', { name: discussion.title }),\n  ).toBeVisible();\n  await expect(page.getByText(discussion.body)).toBeVisible();\n\n  // update discussion:\n  await page.getByRole('button', { name: 'Update Discussion' }).click();\n  await page.getByLabel('Title').click();\n  await page.getByLabel('Title').fill(`${discussion.title} - updated`);\n  await page.getByLabel('Body').click();\n  await page.getByLabel('Body').fill(`${discussion.body} - updated`);\n  await page.getByRole('button', { name: 'Submit' }).click();\n  await page\n    .getByLabel('Discussion Updated')\n    .getByRole('button', { name: 'Close' })\n    .click();\n\n  await expect(\n    page.getByRole('heading', { name: `${discussion.title} - updated` }),\n  ).toBeVisible();\n  await expect(page.getByText(`${discussion.body} - updated`)).toBeVisible();\n\n  // create comment:\n  await page.getByRole('button', { name: 'Create Comment' }).click();\n  await page.getByLabel('Body').click();\n  await page.getByLabel('Body').fill(comment.body);\n  await page.getByRole('button', { name: 'Submit' }).click();\n  await expect(page.getByText(comment.body)).toBeVisible();\n  await page\n    .getByLabel('Comment Created')\n    .getByRole('button', { name: 'Close' })\n    .click();\n\n  // delete comment:\n  await page.getByRole('button', { name: 'Delete Comment' }).click();\n  await expect(\n    page.getByText('Are you sure you want to delete this comment?'),\n  ).toBeVisible();\n  await page.getByRole('button', { name: 'Delete Comment' }).click();\n  await page\n    .getByLabel('Comment Deleted')\n    .getByRole('button', { name: 'Close' })\n    .click();\n  await expect(\n    page.getByRole('heading', { name: 'No Comments Found' }),\n  ).toBeVisible();\n  await expect(page.getByText(comment.body)).toBeHidden();\n\n  // go back to discussions:\n  await page.getByRole('link', { name: 'Discussions' }).click();\n  await page.waitForURL('/app/discussions');\n\n  // delete discussion:\n  await page.getByRole('button', { name: 'Delete Discussion' }).click();\n  await page.getByRole('button', { name: 'Delete Discussion' }).click();\n  await page\n    .getByLabel('Discussion Deleted')\n    .getByRole('button', { name: 'Close' })\n    .click();\n  await expect(\n    page.getByRole('heading', { name: 'No Entries Found' }),\n  ).toBeVisible();\n});\n"
  },
  {
    "path": "apps/nextjs-app/generators/component/component.stories.tsx.hbs",
    "content": "import { Meta, StoryObj } from '@storybook/react';\n\nimport { {{ properCase name }} } from './{{ kebabCase name }}';\n\nconst meta: Meta<typeof {{ properCase name }}> = {\n  component: {{ properCase name }},\n};\n\nexport default meta;\n\ntype Story = StoryObj<typeof {{ properCase name }}>;\n\nexport const Default: Story = {\n  args: {}\n};\n"
  },
  {
    "path": "apps/nextjs-app/generators/component/component.tsx.hbs",
    "content": "import * as React from \"react\"; \n\nexport type {{properCase name}}Props = {};\n\nexport const {{properCase name}} = (props: {{properCase name}}Props) => { \n  return (\n    <div>\n      {{properCase name}}\n    </div>\n  ); \n};"
  },
  {
    "path": "apps/nextjs-app/generators/component/index.cjs",
    "content": "const path = require('path');\nconst fs = require('fs');\n\nconst featuresDir = path.join(process.cwd(), 'src/features');\nconst features = fs.readdirSync(featuresDir);\n\n/**\n *\n * @type {import('plop').PlopGenerator}\n */\nmodule.exports = {\n  description: 'Component Generator',\n  prompts: [\n    {\n      type: 'input',\n      name: 'name',\n      message: 'component name',\n    },\n    {\n      type: 'list',\n      name: 'feature',\n      message: 'Which feature does this component belong to?',\n      choices: ['components', ...features],\n      when: () => features.length > 0,\n    },\n    {\n      type: 'input',\n      name: 'folder',\n      message: 'folder in components',\n      when: ({ feature }) => !feature || feature === 'components',\n    },\n  ],\n  actions: (answers) => {\n    const componentGeneratePath =\n      !answers.feature || answers.feature === 'components'\n        ? 'src/components/{{folder}}'\n        : 'src/features/{{feature}}/components';\n    return [\n      {\n        type: 'add',\n        path: componentGeneratePath + '/{{kebabCase name}}/index.ts',\n        templateFile: 'generators/component/index.ts.hbs',\n      },\n      {\n        type: 'add',\n        path: componentGeneratePath + '/{{kebabCase name}}/{{kebabCase name}}.tsx',\n        templateFile: 'generators/component/component.tsx.hbs',\n      },\n      {\n        type: 'add',\n        path: componentGeneratePath + '/{{kebabCase name}}/{{kebabCase name}}.stories.tsx',\n        templateFile: 'generators/component/component.stories.tsx.hbs',\n      },\n    ];\n  },\n};\n"
  },
  {
    "path": "apps/nextjs-app/generators/component/index.ts.hbs",
    "content": "export * from './{{ kebabCase name }}';\n"
  },
  {
    "path": "apps/nextjs-app/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <link rel=\"icon\" href=\"/favicon.ico\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <meta name=\"theme-color\" content=\"#000000\" />\n    <meta name=\"description\" content=\"Bulletproof React Application\" />\n    <link rel=\"stylesheet\" href=\"https://rsms.me/inter/inter.css\" />\n\n    <title>Bulletproof React</title>\n  </head>\n  <body>\n    <noscript>You need to enable JavaScript to run this app.</noscript>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/nextjs-app/lint-staged.config.mjs",
    "content": "import path from 'path';\n\nconst buildEslintCommand = (filenames) => {\n  return `next lint --fix --file ${filenames\n    .filter((f) => f.includes('/src/'))\n    .map((f) => path.relative(process.cwd(), f))\n    .join(' --file ')}`;\n};\n\nconst config = {\n  '*.{ts,tsx}': [buildEslintCommand, \"bash -c 'yarn check-types'\"],\n};\n\nexport default config;\n"
  },
  {
    "path": "apps/nextjs-app/mock-server.ts",
    "content": "import { createMiddleware } from '@mswjs/http-middleware';\nimport cors from 'cors';\nimport express from 'express';\nimport logger from 'pino-http';\n\nimport { initializeDb } from './src/testing/mocks/db';\nimport { handlers } from './src/testing/mocks/handlers';\n\nconst app = express();\n\napp.use(\n  cors({\n    origin: process.env.NEXT_PUBLIC_URL,\n    credentials: true,\n  }),\n);\n\napp.use(express.json());\napp.use(\n  logger({\n    level: 'info',\n    redact: ['req.headers', 'res.headers'],\n    transport: {\n      target: 'pino-pretty',\n      options: {\n        colorize: true,\n        translateTime: true,\n      },\n    },\n  }),\n);\napp.use(createMiddleware(...handlers));\n\ninitializeDb().then(() => {\n  console.log('Mock DB initialized');\n  app.listen(process.env.NEXT_PUBLIC_MOCK_API_PORT, () => {\n    console.log(\n      `Mock API server started at http://localhost:${process.env.NEXT_PUBLIC_MOCK_API_PORT}`,\n    );\n  });\n});\n"
  },
  {
    "path": "apps/nextjs-app/next-env.d.ts",
    "content": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\n\n// NOTE: This file should not be edited\n// see https://nextjs.org/docs/basic-features/typescript for more information.\n"
  },
  {
    "path": "apps/nextjs-app/next.config.mjs",
    "content": "/** @type {import('next').NextConfig} */\nconst nextConfig = {\n  reactStrictMode: true,\n};\n\nexport default nextConfig;\n"
  },
  {
    "path": "apps/nextjs-app/package.json",
    "content": "{\n  \"name\": \"bulletproof-react-nextjs-pages\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"lint\": \"next lint\",\n    \"test\": \"vitest\",\n    \"test-e2e\": \"pm2 start \\\"yarn run-mock-server\\\" --name server && yarn playwright test\",\n    \"prepare\": \"husky\",\n    \"check-types\": \"tsc --project tsconfig.json --pretty --noEmit\",\n    \"generate\": \"plop\",\n    \"storybook\": \"storybook dev -p 6006\",\n    \"build-storybook\": \"storybook build\",\n    \"run-mock-server\": \"tsx ./mock-server.ts\"\n  },\n  \"dependencies\": {\n    \"@hookform/resolvers\": \"^3.3.4\",\n    \"@next/env\": \"^14.2.5\",\n    \"@ngneat/falso\": \"^7.2.0\",\n    \"@radix-ui/react-dialog\": \"^1.0.5\",\n    \"@radix-ui/react-dropdown-menu\": \"^2.0.6\",\n    \"@radix-ui/react-icons\": \"^1.3.0\",\n    \"@radix-ui/react-label\": \"^2.0.2\",\n    \"@radix-ui/react-slot\": \"^1.0.2\",\n    \"@radix-ui/react-switch\": \"^1.0.3\",\n    \"@tanstack/react-query\": \"^5.32.0\",\n    \"@tanstack/react-query-devtools\": \"^5.32.0\",\n    \"class-variance-authority\": \"^0.7.0\",\n    \"clsx\": \"^2.1.1\",\n    \"dayjs\": \"^1.11.11\",\n    \"eslint-plugin-check-file\": \"^2.8.0\",\n    \"isomorphic-dompurify\": \"^2.14.0\",\n    \"lucide-react\": \"^0.378.0\",\n    \"marked\": \"^12.0.2\",\n    \"nanoid\": \"^5.0.7\",\n    \"next\": \"^14.2.5\",\n    \"react\": \"^18.3.1\",\n    \"react-dom\": \"^18.3.1\",\n    \"react-error-boundary\": \"^4.0.13\",\n    \"react-hook-form\": \"^7.51.3\",\n    \"tailwind-merge\": \"^2.3.0\",\n    \"tailwindcss-animate\": \"^1.0.7\",\n    \"zod\": \"^3.23.4\",\n    \"zustand\": \"^4.5.2\"\n  },\n  \"devDependencies\": {\n    \"@eslint/eslintrc\": \"^3.0.2\",\n    \"@mswjs/data\": \"^0.16.1\",\n    \"@mswjs/http-middleware\": \"^0.10.1\",\n    \"@playwright/test\": \"^1.43.1\",\n    \"@storybook/addon-a11y\": \"^8.0.10\",\n    \"@storybook/addon-actions\": \"^8.0.9\",\n    \"@storybook/addon-essentials\": \"^8.0.9\",\n    \"@storybook/addon-links\": \"^8.0.9\",\n    \"@storybook/nextjs\": \"^8.2.9\",\n    \"@storybook/node-logger\": \"^8.0.9\",\n    \"@storybook/react\": \"^8.0.9\",\n    \"@tailwindcss/typography\": \"^0.5.13\",\n    \"@testing-library/jest-dom\": \"^6.4.2\",\n    \"@testing-library/react\": \"^15.0.5\",\n    \"@testing-library/user-event\": \"^14.5.2\",\n    \"@types/cors\": \"^2.8.17\",\n    \"@types/dompurify\": \"^3.0.5\",\n    \"@types/js-cookie\": \"^3.0.6\",\n    \"@types/marked\": \"^6.0.0\",\n    \"@types/node\": \"^20.12.7\",\n    \"@types/react\": \"^18.3.1\",\n    \"@types/react-dom\": \"^18.3.0\",\n    \"@typescript-eslint/eslint-plugin\": \"^7.8.0\",\n    \"@typescript-eslint/parser\": \"^7.8.0\",\n    \"@vitejs/plugin-react\": \"^4.2.1\",\n    \"autoprefixer\": \"^10.4.19\",\n    \"cors\": \"^2.8.5\",\n    \"dotenv\": \"^16.4.5\",\n    \"eslint\": \"8\",\n    \"eslint-config-next\": \"^14.2.5\",\n    \"eslint-config-prettier\": \"^9.1.0\",\n    \"eslint-import-resolver-typescript\": \"^3.6.1\",\n    \"eslint-plugin-import\": \"^2.29.1\",\n    \"eslint-plugin-jest-dom\": \"^5.4.0\",\n    \"eslint-plugin-jsx-a11y\": \"^6.8.0\",\n    \"eslint-plugin-playwright\": \"^1.6.0\",\n    \"eslint-plugin-prettier\": \"^5.1.3\",\n    \"eslint-plugin-react\": \"^7.34.1\",\n    \"eslint-plugin-react-hooks\": \"^4.6.2\",\n    \"eslint-plugin-tailwindcss\": \"^3.15.1\",\n    \"eslint-plugin-testing-library\": \"^6.2.2\",\n    \"eslint-plugin-vitest\": \"^0.5.4\",\n    \"express\": \"^4.19.2\",\n    \"husky\": \"^9.0.11\",\n    \"jest-environment-jsdom\": \"^29.7.0\",\n    \"js-cookie\": \"^3.0.5\",\n    \"jsdom\": \"^24.0.0\",\n    \"lint-staged\": \"^15.2.2\",\n    \"msw\": \"^2.2.14\",\n    \"pino-http\": \"^10.1.0\",\n    \"pino-pretty\": \"^11.1.0\",\n    \"plop\": \"^4.0.1\",\n    \"pm2\": \"^5.4.0\",\n    \"postcss\": \"^8.4.38\",\n    \"prettier\": \"^3.2.5\",\n    \"storybook\": \"^8.0.9\",\n    \"tailwindcss\": \"^3.4.3\",\n    \"tsx\": \"^4.17.0\",\n    \"typescript\": \"^5.4.5\",\n    \"vite-tsconfig-paths\": \"^4.3.2\",\n    \"vitest\": \"^2.1.4\"\n  },\n  \"msw\": {\n    \"workerDirectory\": \"public\"\n  }\n}\n"
  },
  {
    "path": "apps/nextjs-app/playwright.config.ts",
    "content": "import { defineConfig, devices } from '@playwright/test';\n\nconst PORT = 3000;\n\n/**\n * Read environment variables from file.\n * https://github.com/motdotla/dotenv\n */\n// require('dotenv').config();\n\n/**\n * See https://playwright.dev/docs/test-configuration.\n */\nexport default defineConfig({\n  testDir: './e2e',\n  /* Run tests in files in parallel */\n  fullyParallel: true,\n  /* Fail the build on CI if you accidentally left test.only in the source code. */\n  forbidOnly: !!process.env.CI,\n  /* Retry on CI only */\n  retries: process.env.CI ? 2 : 0,\n  /* Opt out of parallel tests on CI. */\n  workers: process.env.CI ? 1 : undefined,\n  /* Reporter to use. See https://playwright.dev/docs/test-reporters */\n  reporter: 'html',\n  /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */\n  use: {\n    /* Base URL to use in actions like `await page.goto('/')`. */\n    // baseURL: 'http://127.0.0.1:3000',\n\n    /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */\n    trace: 'on-first-retry',\n  },\n\n  /* Configure projects for major browsers */\n  projects: [\n    { name: 'setup', testMatch: /.*\\.setup\\.ts/ },\n    {\n      name: 'chromium',\n      testMatch: /.*\\.spec\\.ts/,\n      use: {\n        ...devices['Desktop Chrome'],\n        storageState: 'e2e/.auth/user.json',\n      },\n      dependencies: ['setup'],\n    },\n  ],\n\n  /* Run your local dev server before starting the tests */\n  webServer: {\n    command: `yarn dev --port ${PORT}`,\n    timeout: 10 * 1000,\n    port: PORT,\n    reuseExistingServer: !process.env.CI,\n  },\n});\n"
  },
  {
    "path": "apps/nextjs-app/plopfile.cjs",
    "content": "const componentGenerator = require('./generators/component/index');\n\n/**\n *\n * @param {import('plop').NodePlopAPI} plop\n */\nmodule.exports = function (plop) {\n  plop.setGenerator('component', componentGenerator);\n};\n"
  },
  {
    "path": "apps/nextjs-app/postcss.config.cjs",
    "content": "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n};\n"
  },
  {
    "path": "apps/nextjs-app/public/_redirects",
    "content": "/* /index.html 200"
  },
  {
    "path": "apps/nextjs-app/public/mockServiceWorker.js",
    "content": "/* eslint-disable */\n/* tslint:disable */\n\n/**\n * Mock Service Worker.\n * @see https://github.com/mswjs/msw\n * - Please do NOT modify this file.\n * - Please do NOT serve this file on production.\n */\n\nconst PACKAGE_VERSION = '2.3.5'\nconst INTEGRITY_CHECKSUM = '26357c79639bfa20d64c0efca2a87423'\nconst IS_MOCKED_RESPONSE = Symbol('isMockedResponse')\nconst activeClientIds = new Set()\n\nself.addEventListener('install', function () {\n  self.skipWaiting()\n})\n\nself.addEventListener('activate', function (event) {\n  event.waitUntil(self.clients.claim())\n})\n\nself.addEventListener('message', async function (event) {\n  const clientId = event.source.id\n\n  if (!clientId || !self.clients) {\n    return\n  }\n\n  const client = await self.clients.get(clientId)\n\n  if (!client) {\n    return\n  }\n\n  const allClients = await self.clients.matchAll({\n    type: 'window',\n  })\n\n  switch (event.data) {\n    case 'KEEPALIVE_REQUEST': {\n      sendToClient(client, {\n        type: 'KEEPALIVE_RESPONSE',\n      })\n      break\n    }\n\n    case 'INTEGRITY_CHECK_REQUEST': {\n      sendToClient(client, {\n        type: 'INTEGRITY_CHECK_RESPONSE',\n        payload: {\n          packageVersion: PACKAGE_VERSION,\n          checksum: INTEGRITY_CHECKSUM,\n        },\n      })\n      break\n    }\n\n    case 'MOCK_ACTIVATE': {\n      activeClientIds.add(clientId)\n\n      sendToClient(client, {\n        type: 'MOCKING_ENABLED',\n        payload: true,\n      })\n      break\n    }\n\n    case 'MOCK_DEACTIVATE': {\n      activeClientIds.delete(clientId)\n      break\n    }\n\n    case 'CLIENT_CLOSED': {\n      activeClientIds.delete(clientId)\n\n      const remainingClients = allClients.filter((client) => {\n        return client.id !== clientId\n      })\n\n      // Unregister itself when there are no more clients\n      if (remainingClients.length === 0) {\n        self.registration.unregister()\n      }\n\n      break\n    }\n  }\n})\n\nself.addEventListener('fetch', function (event) {\n  const { request } = event\n\n  // Bypass navigation requests.\n  if (request.mode === 'navigate') {\n    return\n  }\n\n  // Opening the DevTools triggers the \"only-if-cached\" request\n  // that cannot be handled by the worker. Bypass such requests.\n  if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {\n    return\n  }\n\n  // Bypass all requests when there are no active clients.\n  // Prevents the self-unregistered worked from handling requests\n  // after it's been deleted (still remains active until the next reload).\n  if (activeClientIds.size === 0) {\n    return\n  }\n\n  // Generate unique request ID.\n  const requestId = crypto.randomUUID()\n  event.respondWith(handleRequest(event, requestId))\n})\n\nasync function handleRequest(event, requestId) {\n  const client = await resolveMainClient(event)\n  const response = await getResponse(event, client, requestId)\n\n  // Send back the response clone for the \"response:*\" life-cycle events.\n  // Ensure MSW is active and ready to handle the message, otherwise\n  // this message will pend indefinitely.\n  if (client && activeClientIds.has(client.id)) {\n    ;(async function () {\n      const responseClone = response.clone()\n\n      sendToClient(\n        client,\n        {\n          type: 'RESPONSE',\n          payload: {\n            requestId,\n            isMockedResponse: IS_MOCKED_RESPONSE in response,\n            type: responseClone.type,\n            status: responseClone.status,\n            statusText: responseClone.statusText,\n            body: responseClone.body,\n            headers: Object.fromEntries(responseClone.headers.entries()),\n          },\n        },\n        [responseClone.body],\n      )\n    })()\n  }\n\n  return response\n}\n\n// Resolve the main client for the given event.\n// Client that issues a request doesn't necessarily equal the client\n// that registered the worker. It's with the latter the worker should\n// communicate with during the response resolving phase.\nasync function resolveMainClient(event) {\n  const client = await self.clients.get(event.clientId)\n\n  if (client?.frameType === 'top-level') {\n    return client\n  }\n\n  const allClients = await self.clients.matchAll({\n    type: 'window',\n  })\n\n  return allClients\n    .filter((client) => {\n      // Get only those clients that are currently visible.\n      return client.visibilityState === 'visible'\n    })\n    .find((client) => {\n      // Find the client ID that's recorded in the\n      // set of clients that have registered the worker.\n      return activeClientIds.has(client.id)\n    })\n}\n\nasync function getResponse(event, client, requestId) {\n  const { request } = event\n\n  // Clone the request because it might've been already used\n  // (i.e. its body has been read and sent to the client).\n  const requestClone = request.clone()\n\n  function passthrough() {\n    const headers = Object.fromEntries(requestClone.headers.entries())\n\n    // Remove internal MSW request header so the passthrough request\n    // complies with any potential CORS preflight checks on the server.\n    // Some servers forbid unknown request headers.\n    delete headers['x-msw-intention']\n\n    return fetch(requestClone, { headers })\n  }\n\n  // Bypass mocking when the client is not active.\n  if (!client) {\n    return passthrough()\n  }\n\n  // Bypass initial page load requests (i.e. static assets).\n  // The absence of the immediate/parent client in the map of the active clients\n  // means that MSW hasn't dispatched the \"MOCK_ACTIVATE\" event yet\n  // and is not ready to handle requests.\n  if (!activeClientIds.has(client.id)) {\n    return passthrough()\n  }\n\n  // Notify the client that a request has been intercepted.\n  const requestBuffer = await request.arrayBuffer()\n  const clientMessage = await sendToClient(\n    client,\n    {\n      type: 'REQUEST',\n      payload: {\n        id: requestId,\n        url: request.url,\n        mode: request.mode,\n        method: request.method,\n        headers: Object.fromEntries(request.headers.entries()),\n        cache: request.cache,\n        credentials: request.credentials,\n        destination: request.destination,\n        integrity: request.integrity,\n        redirect: request.redirect,\n        referrer: request.referrer,\n        referrerPolicy: request.referrerPolicy,\n        body: requestBuffer,\n        keepalive: request.keepalive,\n      },\n    },\n    [requestBuffer],\n  )\n\n  switch (clientMessage.type) {\n    case 'MOCK_RESPONSE': {\n      return respondWithMock(clientMessage.data)\n    }\n\n    case 'PASSTHROUGH': {\n      return passthrough()\n    }\n  }\n\n  return passthrough()\n}\n\nfunction sendToClient(client, message, transferrables = []) {\n  return new Promise((resolve, reject) => {\n    const channel = new MessageChannel()\n\n    channel.port1.onmessage = (event) => {\n      if (event.data && event.data.error) {\n        return reject(event.data.error)\n      }\n\n      resolve(event.data)\n    }\n\n    client.postMessage(\n      message,\n      [channel.port2].concat(transferrables.filter(Boolean)),\n    )\n  })\n}\n\nasync function respondWithMock(response) {\n  // Setting response status code to 0 is a no-op.\n  // However, when responding with a \"Response.error()\", the produced Response\n  // instance will have status code set to 0. Since it's not possible to create\n  // a Response instance with status code 0, handle that use-case separately.\n  if (response.status === 0) {\n    return Response.error()\n  }\n\n  const mockedResponse = new Response(response.body, response)\n\n  Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, {\n    value: true,\n    enumerable: true,\n  })\n\n  return mockedResponse\n}\n"
  },
  {
    "path": "apps/nextjs-app/public/robots.txt",
    "content": "# https://www.robotstxt.org/robotstxt.html\nUser-agent: *\nDisallow:\n"
  },
  {
    "path": "apps/nextjs-app/src/app/app/_components/dashboard-info.tsx",
    "content": "'use client';\n\nimport { useUser } from '@/lib/auth';\n\nexport const DashboardInfo = () => {\n  const user = useUser();\n\n  return (\n    <>\n      <h1 className=\"text-xl\">\n        Welcome <b>{`${user.data?.firstName} ${user.data?.lastName}`}</b>\n      </h1>\n      <h4 className=\"my-3\">\n        Your role is : <b>{user.data?.role}</b>\n      </h4>\n      <p className=\"font-medium\">In this application you can:</p>\n      {user.data?.role === 'USER' && (\n        <ul className=\"my-4 list-inside list-disc\">\n          <li>Create comments in discussions</li>\n          <li>Delete own comments</li>\n        </ul>\n      )}\n      {user.data?.role === 'ADMIN' && (\n        <ul className=\"my-4 list-inside list-disc\">\n          <li>Create discussions</li>\n          <li>Edit discussions</li>\n          <li>Delete discussions</li>\n          <li>Comment on discussions</li>\n          <li>Delete all comments</li>\n        </ul>\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/app/app/_components/dashboard-layout.tsx",
    "content": "'use client';\n\nimport { Home, PanelLeft, Folder, Users, User2 } from 'lucide-react';\nimport NextLink from 'next/link';\nimport { useRouter, usePathname } from 'next/navigation';\nimport { ErrorBoundary } from 'react-error-boundary';\n\nimport { Button } from '@/components/ui/button';\nimport { Drawer, DrawerContent, DrawerTrigger } from '@/components/ui/drawer';\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from '@/components/ui/dropdown';\nimport { Link } from '@/components/ui/link';\nimport { paths } from '@/config/paths';\nimport { useLogout, useUser } from '@/lib/auth';\nimport { cn } from '@/utils/cn';\n\ntype SideNavigationItem = {\n  name: string;\n  to: string;\n  icon: (props: React.SVGProps<SVGSVGElement>) => JSX.Element;\n};\n\nconst Logo = () => {\n  return (\n    <Link className=\"flex items-center text-white\" href={paths.home.getHref()}>\n      <img className=\"h-8 w-auto\" src=\"/logo.svg\" alt=\"Workflow\" />\n      <span className=\"text-sm font-semibold text-white\">\n        Bulletproof React\n      </span>\n    </Link>\n  );\n};\n\nconst Layout = ({ children }: { children: React.ReactNode }) => {\n  const user = useUser();\n  const pathname = usePathname();\n  const router = useRouter();\n  const logout = useLogout({\n    onSuccess: () => router.push(paths.auth.login.getHref(pathname)),\n  });\n  const navigation = [\n    { name: 'Dashboard', to: paths.app.root.getHref(), icon: Home },\n    { name: 'Discussions', to: paths.app.discussions.getHref(), icon: Folder },\n    user.data?.role === 'ADMIN' && {\n      name: 'Users',\n      to: paths.app.users.getHref(),\n      icon: Users,\n    },\n  ].filter(Boolean) as SideNavigationItem[];\n\n  return (\n    <div className=\"flex min-h-screen w-full flex-col bg-muted/40\">\n      <aside className=\"fixed inset-y-0 left-0 z-10 hidden w-60 flex-col border-r bg-black sm:flex\">\n        <nav className=\"flex flex-col items-center gap-4 px-2 py-4\">\n          <div className=\"flex h-16 shrink-0 items-center px-4\">\n            <Logo />\n          </div>\n          {navigation.map((item) => {\n            const isActive = pathname === item.to;\n            return (\n              <NextLink\n                key={item.name}\n                href={item.to}\n                className={cn(\n                  'text-gray-300 hover:bg-gray-700 hover:text-white',\n                  'group flex flex-1 w-full items-center rounded-md p-2 text-base font-medium',\n                  isActive && 'bg-gray-900 text-white',\n                )}\n              >\n                <item.icon\n                  className={cn(\n                    'text-gray-400 group-hover:text-gray-300',\n                    'mr-4 size-6 shrink-0',\n                  )}\n                  aria-hidden=\"true\"\n                />\n                {item.name}\n              </NextLink>\n            );\n          })}\n        </nav>\n      </aside>\n      <div className=\"flex flex-col sm:gap-4 sm:py-4 sm:pl-60\">\n        <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\">\n          {/* <Progress /> */}\n          <Drawer>\n            <DrawerTrigger asChild>\n              <Button size=\"icon\" variant=\"outline\" className=\"sm:hidden\">\n                <PanelLeft className=\"size-5\" />\n                <span className=\"sr-only\">Toggle Menu</span>\n              </Button>\n            </DrawerTrigger>\n            <DrawerContent\n              side=\"left\"\n              className=\"bg-black pt-10 text-white sm:max-w-60\"\n            >\n              <nav className=\"grid gap-6 text-lg font-medium\">\n                <div className=\"flex h-16 shrink-0 items-center px-4\">\n                  <Logo />\n                </div>\n                {navigation.map((item) => {\n                  const isActive = pathname === item.to;\n                  return (\n                    <NextLink\n                      key={item.name}\n                      href={item.to}\n                      className={cn(\n                        'text-gray-300 hover:bg-gray-700 hover:text-white',\n                        'group flex flex-1 w-full items-center rounded-md p-2 text-base font-medium',\n                        isActive && 'bg-gray-900 text-white',\n                      )}\n                    >\n                      <item.icon\n                        className={cn(\n                          'text-gray-400 group-hover:text-gray-300',\n                          'mr-4 size-6 shrink-0',\n                        )}\n                        aria-hidden=\"true\"\n                      />\n                      {item.name}\n                    </NextLink>\n                  );\n                })}\n              </nav>\n            </DrawerContent>\n          </Drawer>\n          <DropdownMenu>\n            <DropdownMenuTrigger asChild>\n              <Button\n                variant=\"outline\"\n                size=\"icon\"\n                className=\"overflow-hidden rounded-full\"\n              >\n                <span className=\"sr-only\">Open user menu</span>\n                <User2 className=\"size-6 rounded-full\" />\n              </Button>\n            </DropdownMenuTrigger>\n            <DropdownMenuContent align=\"end\">\n              <DropdownMenuItem\n                onClick={() => router.push(paths.app.profile.getHref())}\n                className={cn('block px-4 py-2 text-sm text-gray-700')}\n              >\n                Your Profile\n              </DropdownMenuItem>\n              <DropdownMenuSeparator />\n              <DropdownMenuItem\n                className={cn('block px-4 py-2 text-sm text-gray-700 w-full')}\n                onClick={() => logout.mutate()}\n              >\n                Sign Out\n              </DropdownMenuItem>\n            </DropdownMenuContent>\n          </DropdownMenu>\n        </header>\n        <main className=\"grid flex-1 items-start gap-4 p-4 sm:px-6 sm:py-0 md:gap-8\">\n          {children}\n        </main>\n      </div>\n    </div>\n  );\n};\n\nfunction Fallback({ error }: { error: Error }) {\n  return <p>Error: {error.message ?? 'Something went wrong!'}</p>;\n}\n\nexport const DashboardLayout = ({\n  children,\n}: {\n  children: React.ReactNode;\n}) => {\n  const pathname = usePathname();\n  return (\n    <Layout>\n      <ErrorBoundary key={pathname} FallbackComponent={Fallback}>\n        {children}\n      </ErrorBoundary>\n    </Layout>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/app/app/discussions/[discussionId]/__tests__/discussion.test.tsx",
    "content": "import { useParams } from 'next/navigation';\n\nimport {\n  renderApp,\n  screen,\n  userEvent,\n  waitFor,\n  createDiscussion,\n  createUser,\n  within,\n  waitForLoadingToFinish,\n} from '@/testing/test-utils';\n\nimport { Discussion } from '../_components/discussion';\n\nvi.mock('next/navigation', async () => {\n  const actual = await vi.importActual('next/navigation');\n  return {\n    ...actual,\n    useRouter: () => {\n      return {\n        push: vi.fn(),\n        replace: vi.fn(),\n      };\n    },\n    useParams: vi.fn(),\n  };\n});\n\nconst renderDiscussion = async () => {\n  const fakeUser = await createUser();\n  const fakeDiscussion = await createDiscussion({ teamId: fakeUser.teamId });\n\n  vi.mocked(useParams).mockReturnValue({ discussionId: fakeDiscussion.id });\n\n  const utils = await renderApp(\n    <Discussion discussionId={fakeDiscussion.id} />,\n    {\n      user: fakeUser,\n      path: `/app/discussions/:discussionId`,\n      url: `/app/discussions/${fakeDiscussion.id}`,\n    },\n  );\n\n  await waitForLoadingToFinish();\n\n  await screen.findByText(fakeDiscussion.title);\n\n  return {\n    ...utils,\n    fakeUser,\n    fakeDiscussion,\n  };\n};\n\ntest('should render discussion', async () => {\n  const { fakeDiscussion } = await renderDiscussion();\n  expect(screen.getByText(fakeDiscussion.body)).toBeInTheDocument();\n});\n\ntest('should update discussion', async () => {\n  const { fakeDiscussion } = await renderDiscussion();\n\n  const titleUpdate = '-Updated';\n  const bodyUpdate = '-Updated';\n\n  await userEvent.click(\n    screen.getByRole('button', { name: /update discussion/i }),\n  );\n\n  const drawer = await screen.findByRole('dialog', {\n    name: /update discussion/i,\n  });\n\n  const titleField = within(drawer).getByText(/title/i);\n  const bodyField = within(drawer).getByText(/body/i);\n\n  const newTitle = `${fakeDiscussion.title}${titleUpdate}`;\n  const newBody = `${fakeDiscussion.body}${bodyUpdate}`;\n\n  // replacing the title with the new title\n  await userEvent.type(titleField, newTitle);\n\n  // appending updated to the body\n  await userEvent.type(bodyField, bodyUpdate);\n\n  const submitButton = within(drawer).getByRole('button', {\n    name: /submit/i,\n  });\n\n  await userEvent.click(submitButton);\n\n  await waitFor(() => expect(drawer).not.toBeInTheDocument());\n\n  expect(\n    await screen.findByRole('heading', { name: newTitle }),\n  ).toBeInTheDocument();\n  expect(await screen.findByText(newBody)).toBeInTheDocument();\n});\n\ntest(\n  'should create and delete a comment on the discussion',\n  async () => {\n    await renderDiscussion();\n\n    const comment = 'Hello World';\n\n    await userEvent.click(\n      screen.getByRole('button', { name: /create comment/i }),\n    );\n\n    const drawer = await screen.findByRole('dialog', {\n      name: /create comment/i,\n    });\n\n    const bodyField = await within(drawer).findByText(/body/i);\n\n    await userEvent.type(bodyField, comment);\n\n    const submitButton = await within(drawer).findByRole('button', {\n      name: /submit/i,\n    });\n\n    await userEvent.click(submitButton);\n\n    await waitFor(() => expect(drawer).not.toBeInTheDocument());\n\n    await screen.findByText(comment);\n\n    const commentsList = await screen.findByRole('list', {\n      name: 'comments',\n    });\n\n    const commentElements =\n      await within(commentsList).findAllByRole('listitem');\n\n    const commentElement = commentElements[0];\n\n    expect(commentElement).toBeInTheDocument();\n\n    const deleteCommentButton = within(commentElement).getByRole('button', {\n      name: /delete comment/i,\n      // exact: false,\n    });\n\n    await userEvent.click(deleteCommentButton);\n\n    const confirmationDialog = await screen.findByRole('dialog', {\n      name: /delete comment/i,\n    });\n\n    const confirmationDeleteButton = await within(\n      confirmationDialog,\n    ).findByRole('button', {\n      name: /delete/i,\n    });\n\n    await userEvent.click(confirmationDeleteButton);\n\n    await screen.findByText(/comment deleted/i);\n\n    await waitFor(() => {\n      expect(within(commentsList).queryByText(comment)).not.toBeInTheDocument();\n    });\n  },\n  {\n    timeout: 20000,\n  },\n);\n"
  },
  {
    "path": "apps/nextjs-app/src/app/app/discussions/[discussionId]/_components/discussion.tsx",
    "content": "'use client';\n\nimport { ErrorBoundary } from 'react-error-boundary';\n\nimport { ContentLayout } from '@/components/layouts/content-layout';\nimport { Comments } from '@/features/comments/components/comments';\nimport { useDiscussion } from '@/features/discussions/api/get-discussion';\nimport { DiscussionView } from '@/features/discussions/components/discussion-view';\n\nexport const Discussion = ({ discussionId }: { discussionId: string }) => {\n  const discussion = useDiscussion({ discussionId });\n\n  return (\n    <ContentLayout title={discussion?.data?.data?.title}>\n      <DiscussionView discussionId={discussionId} />\n      <div className=\"mt-8\">\n        <ErrorBoundary\n          fallback={\n            <div>Failed to load comments. Try to refresh the page.</div>\n          }\n        >\n          <Comments discussionId={discussionId} />\n        </ErrorBoundary>\n      </div>\n    </ContentLayout>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/app/app/discussions/[discussionId]/page.tsx",
    "content": "import {\n  dehydrate,\n  HydrationBoundary,\n  QueryClient,\n} from '@tanstack/react-query';\n\nimport { getInfiniteCommentsQueryOptions } from '@/features/comments/api/get-comments';\nimport {\n  getDiscussion,\n  getDiscussionQueryOptions,\n} from '@/features/discussions/api/get-discussion';\n\nimport { Discussion } from './_components/discussion';\n\nexport const generateMetadata = async ({\n  params,\n}: {\n  params: Promise<{ discussionId: string }>;\n}) => {\n  const discussionId = (await params).discussionId;\n\n  const discussion = await getDiscussion({ discussionId });\n\n  return {\n    title: discussion.data?.title,\n    description: discussion.data?.title,\n  };\n};\n\nconst preloadData = async (discussionId: string) => {\n  const queryClient = new QueryClient();\n\n  await Promise.all([\n    queryClient.prefetchQuery(getDiscussionQueryOptions(discussionId)),\n    queryClient.prefetchInfiniteQuery(\n      getInfiniteCommentsQueryOptions(discussionId),\n    ),\n  ]);\n\n  const dehydratedState = dehydrate(queryClient);\n\n  return {\n    dehydratedState,\n    queryClient,\n  };\n};\n\nconst DiscussionPage = async ({\n  params,\n}: {\n  params: Promise<{\n    discussionId: string;\n  }>;\n}) => {\n  const discussionId = (await params).discussionId;\n\n  const { dehydratedState, queryClient } = await preloadData(discussionId);\n\n  const discussion = queryClient.getQueryData(\n    getDiscussionQueryOptions(discussionId).queryKey,\n  );\n\n  if (!discussion?.data) return <div>Discussion not found</div>;\n\n  return (\n    <HydrationBoundary state={dehydratedState}>\n      <Discussion discussionId={discussionId} />\n    </HydrationBoundary>\n  );\n};\n\nexport default DiscussionPage;\n"
  },
  {
    "path": "apps/nextjs-app/src/app/app/discussions/__tests__/discussions.test.tsx",
    "content": "import type { Mock } from 'vitest';\n\nimport { createDiscussion } from '@/testing/data-generators';\nimport {\n  renderApp,\n  screen,\n  userEvent,\n  waitFor,\n  waitForLoadingToFinish,\n  within,\n} from '@/testing/test-utils';\nimport { formatDate } from '@/utils/format';\n\nimport { Discussions } from '../_components/discussions';\n\nbeforeAll(() => {\n  vi.spyOn(console, 'error').mockImplementation(() => {});\n});\n\nafterAll(() => {\n  (console.error as Mock).mockRestore();\n});\n\ntest(\n  'should create, render and delete discussions',\n  { timeout: 10000 },\n  async () => {\n    await renderApp(<Discussions />);\n\n    await waitForLoadingToFinish();\n\n    const newDiscussion = createDiscussion();\n\n    expect(await screen.findByText(/no entries/i)).toBeInTheDocument();\n\n    await userEvent.click(\n      screen.getByRole('button', { name: /create discussion/i }),\n    );\n\n    const drawer = await screen.findByRole('dialog', {\n      name: /create discussion/i,\n    });\n\n    const titleField = within(drawer).getByText(/title/i);\n    const bodyField = within(drawer).getByText(/body/i);\n\n    await userEvent.type(titleField, newDiscussion.title);\n    await userEvent.type(bodyField, newDiscussion.body);\n\n    const submitButton = within(drawer).getByRole('button', {\n      name: /submit/i,\n    });\n\n    await userEvent.click(submitButton);\n\n    await waitFor(() => expect(drawer).not.toBeInTheDocument());\n\n    const row = await screen.findByRole(\n      'row',\n      {\n        name: `${newDiscussion.title} ${formatDate(newDiscussion.createdAt)} View Delete Discussion`,\n      },\n      { timeout: 5000 },\n    );\n\n    expect(\n      within(row).getByRole('cell', {\n        name: newDiscussion.title,\n      }),\n    ).toBeInTheDocument();\n\n    await userEvent.click(\n      within(row).getByRole('button', {\n        name: /delete discussion/i,\n      }),\n    );\n\n    const confirmationDialog = await screen.findByRole('dialog', {\n      name: /delete discussion/i,\n    });\n\n    const confirmationDeleteButton = within(confirmationDialog).getByRole(\n      'button',\n      {\n        name: /delete discussion/i,\n      },\n    );\n\n    await userEvent.click(confirmationDeleteButton);\n\n    await screen.findByText(/discussion deleted/i);\n\n    expect(\n      within(row).queryByRole('cell', {\n        name: newDiscussion.title,\n      }),\n    ).not.toBeInTheDocument();\n  },\n);\n"
  },
  {
    "path": "apps/nextjs-app/src/app/app/discussions/_components/discussions.tsx",
    "content": "'use client';\n\nimport { useQueryClient } from '@tanstack/react-query';\n\nimport { ContentLayout } from '@/components/layouts/content-layout';\nimport { getInfiniteCommentsQueryOptions } from '@/features/comments/api/get-comments';\nimport { CreateDiscussion } from '@/features/discussions/components/create-discussion';\nimport { DiscussionsList } from '@/features/discussions/components/discussions-list';\n\nexport const Discussions = () => {\n  const queryClient = useQueryClient();\n\n  return (\n    <ContentLayout title=\"Discussions\">\n      <div className=\"flex justify-end\">\n        <CreateDiscussion />\n      </div>\n      <div className=\"mt-4\">\n        <DiscussionsList\n          onDiscussionPrefetch={(id) => {\n            // Prefetch the comments data when the user hovers over the link in the list\n            queryClient.prefetchInfiniteQuery(\n              getInfiniteCommentsQueryOptions(id),\n            );\n          }}\n        />\n      </div>\n    </ContentLayout>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/app/app/discussions/page.tsx",
    "content": "import {\n  dehydrate,\n  HydrationBoundary,\n  QueryClient,\n} from '@tanstack/react-query';\n\nimport { getDiscussionsQueryOptions } from '@/features/discussions/api/get-discussions';\n\nimport { Discussions } from './_components/discussions';\n\nexport const metadata = {\n  title: 'Discussions',\n  description: 'Discussions',\n};\n\nconst DiscussionsPage = async ({\n  searchParams,\n}: {\n  searchParams: { page: string | null };\n}) => {\n  const queryClient = new QueryClient();\n\n  await queryClient.prefetchQuery(\n    getDiscussionsQueryOptions({\n      page: searchParams.page ? Number(searchParams.page) : 1,\n    }),\n  );\n\n  const dehydratedState = dehydrate(queryClient);\n\n  return (\n    <HydrationBoundary state={dehydratedState}>\n      <Discussions />\n    </HydrationBoundary>\n  );\n};\n\nexport default DiscussionsPage;\n"
  },
  {
    "path": "apps/nextjs-app/src/app/app/layout.tsx",
    "content": "import { ReactNode } from 'react';\n\nimport { DashboardLayout } from './_components/dashboard-layout';\n\nexport const metadata = {\n  title: 'Dashboard',\n  description: 'Dashboard',\n};\n\nconst AppLayout = ({ children }: { children: ReactNode }) => {\n  return <DashboardLayout>{children}</DashboardLayout>;\n};\n\nexport default AppLayout;\n"
  },
  {
    "path": "apps/nextjs-app/src/app/app/page.tsx",
    "content": "import { DashboardInfo } from './_components/dashboard-info';\n\nexport const metadata = {\n  title: 'Dashboard',\n  description: 'Dashboard',\n};\n\nconst DashboardPage = async () => {\n  return <DashboardInfo />;\n};\n\nexport default DashboardPage;\n"
  },
  {
    "path": "apps/nextjs-app/src/app/app/profile/_components/profile.tsx",
    "content": "'use client';\n\nimport { UpdateProfile } from '@/features/users/components/update-profile';\nimport { useUser } from '@/lib/auth';\n\ntype EntryProps = {\n  label: string;\n  value: string;\n};\nconst Entry = ({ label, value }: EntryProps) => (\n  <div className=\"py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5\">\n    <dt className=\"text-sm font-medium text-gray-500\">{label}</dt>\n    <dd className=\"mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0\">\n      {value}\n    </dd>\n  </div>\n);\n\nexport const Profile = () => {\n  const user = useUser();\n\n  if (!user) return null;\n\n  return (\n    <div className=\"overflow-hidden bg-white shadow sm:rounded-lg\">\n      <div className=\"px-4 py-5 sm:px-6\">\n        <div className=\"flex justify-between\">\n          <h3 className=\"text-lg font-medium leading-6 text-gray-900\">\n            User Information\n          </h3>\n          <UpdateProfile />\n        </div>\n        <p className=\"mt-1 max-w-2xl text-sm text-gray-500\">\n          Personal details of the user.\n        </p>\n      </div>\n      <div className=\"border-t border-gray-200 px-4 py-5 sm:p-0\">\n        <dl className=\"sm:divide-y sm:divide-gray-200\">\n          <Entry label=\"First Name\" value={user.data?.firstName ?? ''} />\n          <Entry label=\"Last Name\" value={user.data?.lastName ?? ''} />\n          <Entry label=\"Email Address\" value={user.data?.email ?? ''} />\n          <Entry label=\"Role\" value={user.data?.role ?? ''} />\n          <Entry label=\"Bio\" value={user.data?.bio ?? ''} />\n        </dl>\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/app/app/profile/page.tsx",
    "content": "import { Profile } from './_components/profile';\n\nexport const metadata = {\n  title: 'Profile',\n  description: 'Profile',\n};\n\nconst ProfilePage = () => {\n  return <Profile />;\n};\n\nexport default ProfilePage;\n"
  },
  {
    "path": "apps/nextjs-app/src/app/app/users/_components/admin-guard.tsx",
    "content": "'use client';\n\nimport { Spinner } from '@/components/ui/spinner';\nimport { useUser } from '@/lib/auth';\nimport { canViewUsers } from '@/lib/authorization';\n\nexport const AdminGuard = ({ children }: { children: React.ReactNode }) => {\n  const user = useUser();\n\n  if (!user?.data) {\n    return <Spinner className=\"m-4\" />;\n  }\n\n  if (!canViewUsers(user?.data)) {\n    return <div>Only admin can view this.</div>;\n  }\n\n  return children;\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/app/app/users/_components/users.tsx",
    "content": "import {\n  dehydrate,\n  HydrationBoundary,\n  QueryClient,\n} from '@tanstack/react-query';\n\nimport { getUsersQueryOptions } from '@/features/users/api/get-users';\nimport { UsersList } from '@/features/users/components/users-list';\n\nexport const Users = async () => {\n  const queryClient = new QueryClient();\n\n  await queryClient.prefetchQuery(getUsersQueryOptions());\n\n  const dehydratedState = dehydrate(queryClient);\n\n  return (\n    <HydrationBoundary state={dehydratedState}>\n      <UsersList />\n    </HydrationBoundary>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/app/app/users/page.tsx",
    "content": "import { ContentLayout } from '@/components/layouts/content-layout';\n\nimport { AdminGuard } from './_components/admin-guard';\nimport { Users } from './_components/users';\n\nexport const metadata = {\n  title: 'Users',\n  description: 'Users',\n};\n\nconst UsersPage = () => {\n  return (\n    <ContentLayout title=\"Users\">\n      <AdminGuard>\n        <Users />\n      </AdminGuard>\n    </ContentLayout>\n  );\n};\n\nexport default UsersPage;\n"
  },
  {
    "path": "apps/nextjs-app/src/app/auth/_components/auth-layout.tsx",
    "content": "'use client';\n\nimport { useRouter, usePathname, useSearchParams } from 'next/navigation';\nimport { ReactNode, useEffect } from 'react';\n\nimport { Link } from '@/components/ui/link';\nimport { paths } from '@/config/paths';\nimport { useUser } from '@/lib/auth';\n\ntype LayoutProps = {\n  children: ReactNode;\n};\n\nexport const AuthLayout = ({ children }: LayoutProps) => {\n  const user = useUser();\n  const router = useRouter();\n  const pathname = usePathname();\n  const isLoginPage = pathname === paths.auth.login.getHref();\n  const title = isLoginPage\n    ? 'Log in to your account'\n    : 'Register your account';\n\n  const searchParams = useSearchParams();\n  const redirectTo = searchParams?.get('redirectTo');\n\n  useEffect(() => {\n    if (user.data) {\n      router.replace(\n        `${redirectTo ? `${decodeURIComponent(redirectTo)}` : paths.app.dashboard.getHref()}`,\n      );\n    }\n  }, [user.data, router, redirectTo]);\n\n  return (\n    <div className=\"flex min-h-screen flex-col justify-center bg-gray-50 py-12 sm:px-6 lg:px-8\">\n      <div className=\"sm:mx-auto sm:w-full sm:max-w-md\">\n        <div className=\"flex justify-center\">\n          <Link\n            className=\"flex items-center text-white\"\n            href={paths.home.getHref()}\n          >\n            <img className=\"h-24 w-auto\" src=\"/logo.svg\" alt=\"Workflow\" />\n          </Link>\n        </div>\n\n        <h2 className=\"mt-3 text-center text-3xl font-extrabold text-gray-900\">\n          {title}\n        </h2>\n      </div>\n\n      <div className=\"mt-8 sm:mx-auto sm:w-full sm:max-w-md\">\n        <div className=\"bg-white px-4 py-8 shadow sm:rounded-lg sm:px-10\">\n          {children}\n        </div>\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/app/auth/layout.tsx",
    "content": "import { ReactNode, Suspense } from 'react';\nimport { ErrorBoundary } from 'react-error-boundary';\n\nimport { Spinner } from '@/components/ui/spinner';\n\nimport { AuthLayout as AuthLayoutComponent } from './_components/auth-layout';\n\nexport const metadata = {\n  title: 'Bulletproof React',\n  description: 'Welcome to Bulletproof React',\n};\n\nconst AuthLayout = ({ children }: { children: ReactNode }) => {\n  return (\n    <Suspense\n      fallback={\n        <div className=\"flex size-full items-center justify-center\">\n          <Spinner size=\"xl\" />\n        </div>\n      }\n    >\n      <ErrorBoundary fallback={<div>Something went wrong!</div>}>\n        <AuthLayoutComponent>{children}</AuthLayoutComponent>\n      </ErrorBoundary>\n    </Suspense>\n  );\n};\n\nexport default AuthLayout;\n"
  },
  {
    "path": "apps/nextjs-app/src/app/auth/login/page.tsx",
    "content": "'use client';\n\nimport { useRouter, useSearchParams } from 'next/navigation';\n\nimport { paths } from '@/config/paths';\nimport { LoginForm } from '@/features/auth/components/login-form';\n\nconst LoginPage = () => {\n  const router = useRouter();\n  const searchParams = useSearchParams();\n  const redirectTo = searchParams?.get('redirectTo');\n\n  return (\n    <LoginForm\n      onSuccess={() =>\n        router.replace(\n          `${redirectTo ? `${decodeURIComponent(redirectTo)}` : paths.app.dashboard.getHref()}`,\n        )\n      }\n    />\n  );\n};\n\nexport default LoginPage;\n"
  },
  {
    "path": "apps/nextjs-app/src/app/auth/register/page.tsx",
    "content": "'use client';\n\nimport { useRouter, useSearchParams } from 'next/navigation';\nimport { useState } from 'react';\n\nimport { paths } from '@/config/paths';\nimport { RegisterForm } from '@/features/auth/components/register-form';\nimport { useTeams } from '@/features/teams/api/get-teams';\n\nconst RegisterPage = () => {\n  const router = useRouter();\n\n  const searchParams = useSearchParams();\n  const redirectTo = searchParams?.get('redirectTo');\n\n  const [chooseTeam, setChooseTeam] = useState(false);\n\n  const teamsQuery = useTeams({\n    queryConfig: {\n      enabled: chooseTeam,\n    },\n  });\n\n  return (\n    <RegisterForm\n      onSuccess={() =>\n        router.replace(\n          `${redirectTo ? `${decodeURIComponent(redirectTo)}` : paths.app.dashboard.getHref()}`,\n        )\n      }\n      chooseTeam={chooseTeam}\n      setChooseTeam={() => setChooseTeam(!chooseTeam)}\n      teams={teamsQuery.data?.data}\n    />\n  );\n};\n\nexport default RegisterPage;\n"
  },
  {
    "path": "apps/nextjs-app/src/app/layout.tsx",
    "content": "import {\n  dehydrate,\n  HydrationBoundary,\n  QueryClient,\n} from '@tanstack/react-query';\nimport { ReactNode } from 'react';\n\nimport { AppProvider } from '@/app/provider';\nimport { getUserQueryOptions } from '@/lib/auth';\n\nimport '@/styles/globals.css';\n\nexport const metadata = {\n  title: 'Bulletproof React',\n  description: 'Showcasing Best Practices For Building React Applications',\n};\n\nconst RootLayout = async ({ children }: { children: ReactNode }) => {\n  const queryClient = new QueryClient();\n\n  await queryClient.prefetchQuery(getUserQueryOptions());\n\n  const dehydratedState = dehydrate(queryClient);\n\n  return (\n    <html lang=\"en\">\n      <body>\n        <AppProvider>\n          <HydrationBoundary state={dehydratedState}>\n            {children}\n          </HydrationBoundary>\n        </AppProvider>\n      </body>\n    </html>\n  );\n};\n\nexport default RootLayout;\n\n// We are not prerendering anything because the app is highly dynamic\n// and the data depends on the user so we need to send cookies with each request\nexport const dynamic = 'force-dynamic';\n"
  },
  {
    "path": "apps/nextjs-app/src/app/not-found.tsx",
    "content": "import { Link } from '@/components/ui/link';\nimport { paths } from '@/config/paths';\n\nconst NotFoundPage = () => {\n  return (\n    <div className=\"mt-52 flex flex-col items-center font-semibold\">\n      <h1>404 - Not Found</h1>\n      <p>Sorry, the page you are looking for does not exist.</p>\n      <Link href={paths.home.getHref()} replace>\n        Go to Home\n      </Link>\n    </div>\n  );\n};\n\nexport default NotFoundPage;\n"
  },
  {
    "path": "apps/nextjs-app/src/app/page.tsx",
    "content": "import { Button } from '@/components/ui/button';\nimport { Link } from '@/components/ui/link';\nimport { paths } from '@/config/paths';\nimport { checkLoggedIn } from '@/utils/auth';\n\nconst HomePage = () => {\n  const isLoggedIn = checkLoggedIn();\n\n  return (\n    <div className=\"flex h-screen items-center bg-white\">\n      <div className=\"mx-auto max-w-7xl px-4 py-12 text-center sm:px-6 lg:px-8 lg:py-16\">\n        <h2 className=\"text-3xl font-extrabold tracking-tight text-gray-900 sm:text-4xl\">\n          <span className=\"block\">Bulletproof React</span>\n        </h2>\n        <img src=\"/logo.svg\" alt=\"react\" />\n        <p>Showcasing Best Practices For Building React Applications</p>\n        <div className=\"mt-8 flex justify-center\">\n          <div className=\"inline-flex rounded-md shadow\">\n            <Link\n              href={\n                isLoggedIn\n                  ? paths.app.root.getHref()\n                  : paths.auth.login.getHref()\n              }\n            >\n              <Button\n                icon={\n                  <svg\n                    xmlns=\"http://www.w3.org/2000/svg\"\n                    className=\"size-6\"\n                    fill=\"none\"\n                    viewBox=\"0 0 24 24\"\n                    stroke=\"currentColor\"\n                  >\n                    <path\n                      strokeLinecap=\"round\"\n                      strokeLinejoin=\"round\"\n                      strokeWidth=\"2\"\n                      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\"\n                    />\n                  </svg>\n                }\n              >\n                Get started\n              </Button>\n            </Link>\n          </div>\n          <div className=\"ml-3 inline-flex\">\n            <a\n              href=\"https://github.com/alan2207/bulletproof-react\"\n              target=\"_blank\"\n              rel=\"noreferrer\"\n            >\n              <Button\n                variant=\"outline\"\n                icon={\n                  <svg\n                    fill=\"currentColor\"\n                    viewBox=\"0 0 24 24\"\n                    className=\"size-6\"\n                  >\n                    <path\n                      fillRule=\"evenodd\"\n                      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\"\n                      clipRule=\"evenodd\"\n                    />\n                  </svg>\n                }\n              >\n                Github Repo\n              </Button>\n            </a>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default HomePage;\n"
  },
  {
    "path": "apps/nextjs-app/src/app/provider.tsx",
    "content": "'use client';\n\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query';\nimport { ReactQueryDevtools } from '@tanstack/react-query-devtools';\nimport * as React from 'react';\nimport { ErrorBoundary } from 'react-error-boundary';\n\nimport { MainErrorFallback } from '@/components/errors/main';\nimport { Notifications } from '@/components/ui/notifications';\nimport { queryConfig } from '@/lib/react-query';\n\ntype AppProviderProps = {\n  children: React.ReactNode;\n};\n\nexport const AppProvider = ({ children }: AppProviderProps) => {\n  const [queryClient] = React.useState(\n    () =>\n      new QueryClient({\n        defaultOptions: queryConfig,\n      }),\n  );\n\n  return (\n    <ErrorBoundary FallbackComponent={MainErrorFallback}>\n      <QueryClientProvider client={queryClient}>\n        {process.env.DEV && <ReactQueryDevtools />}\n        <Notifications />\n        {children}\n      </QueryClientProvider>\n    </ErrorBoundary>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/app/public/discussions/[discussionId]/page.tsx",
    "content": "import {\n  dehydrate,\n  HydrationBoundary,\n  QueryClient,\n} from '@tanstack/react-query';\n\nimport { Discussion } from '@/app/app/discussions/[discussionId]/_components/discussion';\nimport { getInfiniteCommentsQueryOptions } from '@/features/comments/api/get-comments';\nimport {\n  getDiscussion,\n  getDiscussionQueryOptions,\n} from '@/features/discussions/api/get-discussion';\n\nexport const generateMetadata = async ({\n  params,\n}: {\n  params: Promise<{ discussionId: string }>;\n}) => {\n  const discussionId = (await params).discussionId;\n\n  const discussion = await getDiscussion({ discussionId });\n\n  return {\n    title: discussion.data?.title,\n    description: discussion.data?.title,\n  };\n};\n\nconst preloadData = async (discussionId: string) => {\n  const queryClient = new QueryClient();\n\n  await Promise.all([\n    queryClient.prefetchQuery(getDiscussionQueryOptions(discussionId)),\n    queryClient.prefetchInfiniteQuery(\n      getInfiniteCommentsQueryOptions(discussionId),\n    ),\n  ]);\n\n  return {\n    dehydratedState: dehydrate(queryClient),\n  };\n};\n\nconst PublicDiscussionPage = async ({\n  params: { discussionId },\n}: {\n  params: {\n    discussionId: string;\n  };\n}) => {\n  const { dehydratedState } = await preloadData(discussionId);\n  return (\n    <HydrationBoundary state={dehydratedState}>\n      <Discussion discussionId={discussionId} />\n    </HydrationBoundary>\n  );\n};\n\nexport default PublicDiscussionPage;\n"
  },
  {
    "path": "apps/nextjs-app/src/components/errors/main.tsx",
    "content": "import { Button } from '../ui/button';\n\nexport const MainErrorFallback = () => {\n  return (\n    <div\n      className=\"flex h-screen w-screen flex-col items-center justify-center text-red-500\"\n      role=\"alert\"\n    >\n      <h2 className=\"text-lg font-semibold\">Ooops, something went wrong :( </h2>\n      <Button\n        className=\"mt-4\"\n        onClick={() => window.location.assign(window.location.origin)}\n      >\n        Refresh\n      </Button>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/components/layouts/content-layout.tsx",
    "content": "import { ReactNode } from 'react';\n\ntype ContentLayoutProps = {\n  children: ReactNode;\n  title?: string;\n};\n\nexport const ContentLayout = ({ children, title = '' }: ContentLayoutProps) => {\n  return (\n    <div className=\"py-6\">\n      <div className=\"mx-auto max-w-7xl px-4 sm:px-6 md:px-8\">\n        <h1 className=\"text-2xl font-semibold text-gray-900\">{title}</h1>\n      </div>\n      <div className=\"mx-auto max-w-7xl px-4 py-6 sm:px-6 md:px-8\">\n        {children}\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/button/button.stories.tsx",
    "content": "import { Meta, StoryObj } from '@storybook/react';\n\nimport { Button } from './button';\n\nconst meta: Meta<typeof Button> = {\n  component: Button,\n};\n\nexport default meta;\ntype Story = StoryObj<typeof Button>;\n\nexport const Default: Story = {\n  args: {\n    children: 'Button',\n    variant: 'default',\n  },\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/button/button.tsx",
    "content": "import { Slot } from '@radix-ui/react-slot';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport * as React from 'react';\n\nimport { cn } from '@/utils/cn';\n\nimport { Spinner } from '../spinner';\n\nconst buttonVariants = cva(\n  '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',\n  {\n    variants: {\n      variant: {\n        default:\n          'bg-primary text-primary-foreground shadow hover:bg-primary/90',\n        destructive:\n          'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',\n        outline:\n          'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',\n        secondary:\n          'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',\n        ghost: 'hover:bg-accent hover:text-accent-foreground',\n        link: 'text-primary underline-offset-4 hover:underline',\n      },\n      size: {\n        default: 'h-9 px-4 py-2',\n        sm: 'h-8 rounded-md px-3 text-xs',\n        lg: 'h-10 rounded-md px-8',\n        icon: 'size-9',\n      },\n    },\n    defaultVariants: {\n      variant: 'default',\n      size: 'default',\n    },\n  },\n);\n\nexport type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> &\n  VariantProps<typeof buttonVariants> & {\n    asChild?: boolean;\n    isLoading?: boolean;\n    icon?: React.ReactNode;\n  };\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n  (\n    {\n      className,\n      variant,\n      size,\n      asChild = false,\n      children,\n      isLoading,\n      icon,\n      ...props\n    },\n    ref,\n  ) => {\n    const Comp = asChild ? Slot : 'button';\n    return (\n      <Comp\n        className={cn(buttonVariants({ variant, size, className }))}\n        ref={ref}\n        {...props}\n      >\n        {isLoading && <Spinner size=\"sm\" className=\"text-current\" />}\n        {!isLoading && icon && <span className=\"mr-2\">{icon}</span>}\n        <span className=\"mx-2\">{children}</span>\n      </Comp>\n    );\n  },\n);\nButton.displayName = 'Button';\n\nexport { Button, buttonVariants };\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/button/index.ts",
    "content": "export * from './button';\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/dialog/__tests__/dialog.test.tsx",
    "content": "import * as React from 'react';\n\nimport { Button } from '@/components/ui/button';\nimport { useDisclosure } from '@/hooks/use-disclosure';\nimport { rtlRender, screen, userEvent, waitFor } from '@/testing/test-utils';\n\nimport {\n  Dialog,\n  DialogContent,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from '../dialog';\n\nconst openButtonText = 'Open Modal';\nconst cancelButtonText = 'Cancel';\nconst titleText = 'Modal Title';\n\nconst TestDialog = () => {\n  const { close, open, isOpen } = useDisclosure();\n  const cancelButtonRef = React.useRef(null);\n\n  return (\n    <Dialog\n      open={isOpen}\n      onOpenChange={(isOpen) => {\n        if (!isOpen) {\n          close();\n        } else {\n          open();\n        }\n      }}\n    >\n      <DialogTrigger asChild>\n        <Button variant=\"outline\">{openButtonText}</Button>\n      </DialogTrigger>\n      <DialogContent className=\"sm:max-w-[425px]\">\n        <DialogHeader>\n          <DialogTitle>{titleText}</DialogTitle>\n        </DialogHeader>\n\n        <DialogFooter>\n          <Button type=\"submit\">Submit</Button>\n          <Button ref={cancelButtonRef} variant=\"outline\" onClick={close}>\n            {cancelButtonText}\n          </Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n};\n\ntest('should handle basic dialog flow', async () => {\n  rtlRender(<TestDialog />);\n\n  expect(screen.queryByText(titleText)).not.toBeInTheDocument();\n\n  await userEvent.click(screen.getByRole('button', { name: openButtonText }));\n\n  expect(await screen.findByText(titleText)).toBeInTheDocument();\n\n  await userEvent.click(screen.getByRole('button', { name: cancelButtonText }));\n\n  await waitFor(() =>\n    expect(screen.queryByText(titleText)).not.toBeInTheDocument(),\n  );\n});\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/dialog/confirmation-dialog/__tests__/confirmation-dialog.test.tsx",
    "content": "import { Button } from '@/components/ui/button';\nimport { rtlRender, screen, userEvent, waitFor } from '@/testing/test-utils';\n\nimport { ConfirmationDialog } from '../confirmation-dialog';\n\ntest('should handle confirmation flow', async () => {\n  const titleText = 'Are you sure?';\n  const bodyText = 'Are you sure you want to delete this item?';\n  const confirmationButtonText = 'Confirm';\n  const openButtonText = 'Open';\n\n  await rtlRender(\n    <ConfirmationDialog\n      icon=\"danger\"\n      title={titleText}\n      body={bodyText}\n      confirmButton={<Button>{confirmationButtonText}</Button>}\n      triggerButton={<Button>{openButtonText}</Button>}\n    />,\n  );\n\n  expect(screen.queryByText(titleText)).not.toBeInTheDocument();\n\n  await userEvent.click(screen.getByRole('button', { name: openButtonText }));\n\n  expect(await screen.findByText(titleText)).toBeInTheDocument();\n\n  expect(screen.getByText(bodyText)).toBeInTheDocument();\n\n  await userEvent.click(screen.getByRole('button', { name: 'Cancel' }));\n\n  await waitFor(() =>\n    expect(screen.queryByText(titleText)).not.toBeInTheDocument(),\n  );\n\n  expect(screen.queryByText(bodyText)).not.toBeInTheDocument();\n});\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/dialog/confirmation-dialog/confirmation-dialog.stories.tsx",
    "content": "import { Meta, StoryObj } from '@storybook/react';\n\nimport { Button } from '@/components/ui/button';\n\nimport { ConfirmationDialog } from './confirmation-dialog';\n\nconst meta: Meta<typeof ConfirmationDialog> = {\n  component: ConfirmationDialog,\n};\n\nexport default meta;\n\ntype Story = StoryObj<typeof ConfirmationDialog>;\n\nexport const Danger: Story = {\n  args: {\n    icon: 'danger',\n    title: 'Confirmation',\n    body: 'Hello World',\n    confirmButton: <Button className=\"bg-red-500\">Confirm</Button>,\n    triggerButton: <Button>Open</Button>,\n  },\n};\n\nexport const Info: Story = {\n  args: {\n    icon: 'info',\n    title: 'Confirmation',\n    body: 'Hello World',\n    confirmButton: <Button>Confirm</Button>,\n    triggerButton: <Button>Open</Button>,\n  },\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/dialog/confirmation-dialog/confirmation-dialog.tsx",
    "content": "'use client';\n\nimport { CircleAlert, Info } from 'lucide-react';\nimport * as React from 'react';\nimport { useEffect } from 'react';\n\nimport { Button } from '@/components/ui/button';\nimport { useDisclosure } from '@/hooks/use-disclosure';\n\nimport {\n  Dialog,\n  DialogContent,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from '../dialog';\n\nexport type ConfirmationDialogProps = {\n  triggerButton: React.ReactElement;\n  confirmButton: React.ReactElement;\n  title: string;\n  body?: string;\n  cancelButtonText?: string;\n  icon?: 'danger' | 'info';\n  isDone?: boolean;\n};\n\nexport const ConfirmationDialog = ({\n  triggerButton,\n  confirmButton,\n  title,\n  body = '',\n  cancelButtonText = 'Cancel',\n  icon = 'danger',\n  isDone = false,\n}: ConfirmationDialogProps) => {\n  const { close, open, isOpen } = useDisclosure();\n  const cancelButtonRef = React.useRef(null);\n\n  useEffect(() => {\n    if (isDone) {\n      close();\n    }\n  }, [isDone, close]);\n\n  return (\n    <Dialog\n      open={isOpen}\n      onOpenChange={(isOpen) => {\n        if (!isOpen) {\n          close();\n        } else {\n          open();\n        }\n      }}\n    >\n      <DialogTrigger asChild>{triggerButton}</DialogTrigger>\n      <DialogContent className=\"sm:max-w-[425px]\">\n        <DialogHeader className=\"flex\">\n          <DialogTitle className=\"flex items-center gap-2\">\n            {' '}\n            {icon === 'danger' && (\n              <CircleAlert className=\"size-6 text-red-600\" aria-hidden=\"true\" />\n            )}\n            {icon === 'info' && (\n              <Info className=\"size-6 text-blue-600\" aria-hidden=\"true\" />\n            )}\n            {title}\n          </DialogTitle>\n        </DialogHeader>\n\n        <div className=\"mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left\">\n          {body && (\n            <div className=\"mt-2\">\n              <p>{body}</p>\n            </div>\n          )}\n        </div>\n\n        <DialogFooter>\n          {confirmButton}\n          <Button ref={cancelButtonRef} variant=\"outline\" onClick={close}>\n            {cancelButtonText}\n          </Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/dialog/confirmation-dialog/index.ts",
    "content": "export * from './confirmation-dialog';\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/dialog/dialog.stories.tsx",
    "content": "import { Meta, StoryObj } from '@storybook/react';\nimport * as React from 'react';\n\nimport { Button } from '@/components/ui/button';\nimport { useDisclosure } from '@/hooks/use-disclosure';\n\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from './dialog';\n\nconst DemoDialog = () => {\n  const { close, open, isOpen } = useDisclosure();\n  const cancelButtonRef = React.useRef(null);\n\n  return (\n    <Dialog\n      open={isOpen}\n      onOpenChange={(isOpen) => {\n        if (!isOpen) {\n          close();\n        } else {\n          open();\n        }\n      }}\n    >\n      <DialogTrigger asChild>\n        <Button variant=\"outline\">Open Dialog</Button>\n      </DialogTrigger>\n      <DialogContent className=\"sm:max-w-[425px]\">\n        <DialogHeader>\n          <DialogTitle>Edit profile</DialogTitle>\n          <DialogDescription>Lorem ipsum</DialogDescription>\n        </DialogHeader>\n        <div className=\"grid gap-4 py-4\">Lorem ipsum</div>\n\n        <DialogFooter>\n          <Button type=\"submit\">Save changes</Button>\n          <Button ref={cancelButtonRef} variant=\"outline\" onClick={close}>\n            Cancel\n          </Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n};\n\nconst meta: Meta = {\n  component: Dialog,\n};\n\nexport default meta;\n\ntype Story = StoryObj<typeof Dialog>;\n\nexport const Demo: Story = {\n  render: () => <DemoDialog />,\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/dialog/dialog.tsx",
    "content": "'use client';\n\nimport * as DialogPrimitive from '@radix-ui/react-dialog';\nimport { Cross2Icon } from '@radix-ui/react-icons';\nimport * as React from 'react';\n\nimport { cn } from '@/utils/cn';\n\nconst Dialog = DialogPrimitive.Root;\n\nconst DialogTrigger = DialogPrimitive.Trigger;\n\nconst DialogPortal = DialogPrimitive.Portal;\n\nconst DialogClose = DialogPrimitive.Close;\n\nconst DialogOverlay = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Overlay\n    ref={ref}\n    className={cn(\n      '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',\n      className,\n    )}\n    {...props}\n  />\n));\nDialogOverlay.displayName = DialogPrimitive.Overlay.displayName;\n\nconst DialogContent = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n  <DialogPortal>\n    <DialogOverlay />\n    <DialogPrimitive.Content\n      ref={ref}\n      className={cn(\n        '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',\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      <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\">\n        <Cross2Icon className=\"size-4\" />\n        <span className=\"sr-only\">Close</span>\n      </DialogPrimitive.Close>\n    </DialogPrimitive.Content>\n  </DialogPortal>\n));\nDialogContent.displayName = DialogPrimitive.Content.displayName;\n\nconst DialogHeader = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      'flex flex-col space-y-1.5 text-center sm:text-left',\n      className,\n    )}\n    {...props}\n  />\n);\nDialogHeader.displayName = 'DialogHeader';\n\nconst DialogFooter = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',\n      className,\n    )}\n    {...props}\n  />\n);\nDialogFooter.displayName = 'DialogFooter';\n\nconst DialogTitle = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Title\n    ref={ref}\n    className={cn(\n      'text-lg font-semibold leading-none tracking-tight',\n      className,\n    )}\n    {...props}\n  />\n));\nDialogTitle.displayName = DialogPrimitive.Title.displayName;\n\nconst DialogDescription = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Description\n    ref={ref}\n    className={cn('text-sm text-muted-foreground', className)}\n    {...props}\n  />\n));\nDialogDescription.displayName = DialogPrimitive.Description.displayName;\n\nexport {\n  Dialog,\n  DialogPortal,\n  DialogOverlay,\n  DialogTrigger,\n  DialogClose,\n  DialogContent,\n  DialogHeader,\n  DialogFooter,\n  DialogTitle,\n  DialogDescription,\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/dialog/index.ts",
    "content": "export * from './dialog';\nexport * from './confirmation-dialog';\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/drawer/__tests__/drawer.test.tsx",
    "content": "import { Button } from '@/components/ui/button';\nimport { rtlRender, screen, userEvent, waitFor } from '@/testing/test-utils';\n\nimport {\n  Drawer,\n  DrawerClose,\n  DrawerContent,\n  DrawerFooter,\n  DrawerHeader,\n  DrawerTitle,\n  DrawerTrigger,\n} from '../drawer';\n\nconst openButtonText = 'Open Drawer';\nconst titleText = 'Drawer Title';\nconst cancelButtonText = 'Cancel';\nconst drawerContentText = 'Hello From Drawer';\n\nconst TestDrawer = () => {\n  return (\n    <Drawer>\n      <DrawerTrigger asChild>\n        <Button variant=\"outline\">{openButtonText}</Button>\n      </DrawerTrigger>\n      <DrawerContent className=\"flex max-w-[800px] flex-col justify-between sm:max-w-[540px]\">\n        <div className=\"flex flex-col\">\n          <DrawerHeader>\n            <DrawerTitle>{titleText}</DrawerTitle>\n          </DrawerHeader>\n          <div>{drawerContentText}</div>\n        </div>\n        <DrawerFooter>\n          <DrawerClose asChild>\n            <Button value=\"outline\" type=\"submit\">\n              {cancelButtonText}\n            </Button>\n          </DrawerClose>\n        </DrawerFooter>\n      </DrawerContent>\n    </Drawer>\n  );\n};\n\ntest('should handle basic drawer flow', async () => {\n  await rtlRender(<TestDrawer />);\n\n  expect(screen.queryByText(titleText)).not.toBeInTheDocument();\n\n  await userEvent.click(\n    screen.getByRole('button', {\n      name: openButtonText,\n    }),\n  );\n\n  expect(await screen.findByText(titleText)).toBeInTheDocument();\n\n  await userEvent.click(\n    screen.getByRole('button', {\n      name: cancelButtonText,\n    }),\n  );\n\n  await waitFor(() =>\n    expect(screen.queryByText(titleText)).not.toBeInTheDocument(),\n  );\n});\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/drawer/drawer.stories.tsx",
    "content": "import { Meta, StoryObj } from '@storybook/react';\n\nimport { Button } from '@/components/ui/button';\nimport { useDisclosure } from '@/hooks/use-disclosure';\n\nimport {\n  Drawer,\n  DrawerClose,\n  DrawerContent,\n  DrawerDescription,\n  DrawerFooter,\n  DrawerHeader,\n  DrawerTitle,\n  DrawerTrigger,\n} from './drawer';\n\nconst meta: Meta<typeof Drawer> = {\n  component: Drawer,\n};\n\nexport default meta;\n\ntype Story = StoryObj<typeof Drawer>;\n\nconst DemoDrawer = () => {\n  const { close, open, isOpen } = useDisclosure();\n\n  return (\n    <Drawer\n      open={isOpen}\n      onOpenChange={(isOpen) => {\n        if (!isOpen) {\n          close();\n        } else {\n          open();\n        }\n      }}\n    >\n      <DrawerTrigger asChild>\n        <Button variant=\"outline\">Open</Button>\n      </DrawerTrigger>\n      <DrawerContent className=\"flex max-w-[800px] flex-col justify-between sm:max-w-[540px]\">\n        <div className=\"flex flex-col\">\n          <DrawerHeader>\n            <DrawerTitle>Drawer Header</DrawerTitle>\n            <DrawerDescription>\n              Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n            </DrawerDescription>\n          </DrawerHeader>\n          <div>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</div>\n        </div>\n        <DrawerFooter>\n          <DrawerClose asChild>\n            <Button type=\"submit\">Save changes</Button>\n          </DrawerClose>\n        </DrawerFooter>\n      </DrawerContent>\n    </Drawer>\n  );\n};\n\nexport const Default: Story = {\n  render: () => <DemoDrawer />,\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/drawer/drawer.tsx",
    "content": "'use client';\n\nimport * as DrawerPrimitive from '@radix-ui/react-dialog';\nimport { Cross2Icon } from '@radix-ui/react-icons';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport * as React from 'react';\n\nimport { cn } from '@/utils/cn';\n\nconst Drawer = DrawerPrimitive.Root;\n\nconst DrawerTrigger = DrawerPrimitive.Trigger;\n\nconst DrawerClose = DrawerPrimitive.Close;\n\nconst DrawerPortal = DrawerPrimitive.Portal;\n\nconst DrawerOverlay = React.forwardRef<\n  React.ElementRef<typeof DrawerPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <DrawerPrimitive.Overlay\n    className={cn(\n      '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',\n      className,\n    )}\n    {...props}\n    ref={ref}\n  />\n));\nDrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;\n\nconst drawerVariants = cva(\n  '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',\n  {\n    variants: {\n      side: {\n        top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',\n        bottom:\n          'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',\n        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',\n        right:\n          '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',\n      },\n    },\n    defaultVariants: {\n      side: 'right',\n    },\n  },\n);\n\ntype DrawerContentProps = React.ComponentPropsWithoutRef<\n  typeof DrawerPrimitive.Content\n> &\n  VariantProps<typeof drawerVariants>;\n\nconst DrawerContent = React.forwardRef<\n  React.ElementRef<typeof DrawerPrimitive.Content>,\n  DrawerContentProps\n>(({ side = 'right', className, children, ...props }, ref) => (\n  <DrawerPortal>\n    <DrawerOverlay />\n    <DrawerPrimitive.Content\n      ref={ref}\n      className={cn(drawerVariants({ side }), className)}\n      {...props}\n    >\n      {children}\n      <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\">\n        <Cross2Icon className=\"size-4\" />\n        <span className=\"sr-only\">Close</span>\n      </DrawerPrimitive.Close>\n    </DrawerPrimitive.Content>\n  </DrawerPortal>\n));\nDrawerContent.displayName = DrawerPrimitive.Content.displayName;\n\nconst DrawerHeader = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      'flex flex-col space-y-2 text-center sm:text-left',\n      className,\n    )}\n    {...props}\n  />\n);\nDrawerHeader.displayName = 'DrawerHeader';\n\nconst DrawerFooter = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',\n      className,\n    )}\n    {...props}\n  />\n);\nDrawerFooter.displayName = 'DrawerFooter';\n\nconst DrawerTitle = React.forwardRef<\n  React.ElementRef<typeof DrawerPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <DrawerPrimitive.Title\n    ref={ref}\n    className={cn('text-lg font-semibold text-foreground', className)}\n    {...props}\n  />\n));\nDrawerTitle.displayName = DrawerPrimitive.Title.displayName;\n\nconst DrawerDescription = React.forwardRef<\n  React.ElementRef<typeof DrawerPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <DrawerPrimitive.Description\n    ref={ref}\n    className={cn('text-sm text-muted-foreground', className)}\n    {...props}\n  />\n));\nDrawerDescription.displayName = DrawerPrimitive.Description.displayName;\n\nexport {\n  Drawer,\n  DrawerPortal,\n  DrawerOverlay,\n  DrawerTrigger,\n  DrawerClose,\n  DrawerContent,\n  DrawerHeader,\n  DrawerFooter,\n  DrawerTitle,\n  DrawerDescription,\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/drawer/index.ts",
    "content": "export * from './drawer';\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/dropdown/dropdown.stories.tsx",
    "content": "import type { Meta } from '@storybook/react';\nimport React from 'react';\n\nimport { Button } from '@/components/ui/button';\n\nimport {\n  DropdownMenu,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuCheckboxItem,\n  DropdownMenuRadioItem,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuSub,\n  DropdownMenuSubTrigger,\n  DropdownMenuSubContent,\n  DropdownMenuRadioGroup,\n} from './dropdown';\n\nconst meta: Meta = {\n  component: DropdownMenu,\n};\n\nexport default meta;\n\nexport const Default = () => (\n  <DropdownMenu>\n    <DropdownMenuTrigger asChild>\n      <Button>Open Menu</Button>\n    </DropdownMenuTrigger>\n    <DropdownMenuContent>\n      <DropdownMenuItem>Item One</DropdownMenuItem>\n      <DropdownMenuItem>Item Two</DropdownMenuItem>\n      <DropdownMenuSeparator />\n      <DropdownMenuItem>Item Three</DropdownMenuItem>\n    </DropdownMenuContent>\n  </DropdownMenu>\n);\n\nexport const WithCheckboxItems = () => {\n  const [checked, setChecked] = React.useState(true);\n  const [checked2, setChecked2] = React.useState(false);\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <Button>Open Menu</Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent>\n        <DropdownMenuCheckboxItem\n          checked={checked}\n          onCheckedChange={setChecked}\n        >\n          Option One\n        </DropdownMenuCheckboxItem>\n        <DropdownMenuCheckboxItem\n          checked={checked2}\n          onCheckedChange={setChecked2}\n        >\n          Option Two\n        </DropdownMenuCheckboxItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n};\n\nexport const WithRadioItems = () => {\n  const [value, setValue] = React.useState('one');\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <Button>Open Menu</Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent>\n        <DropdownMenuLabel>Select an option</DropdownMenuLabel>\n        <DropdownMenuSeparator />\n        <DropdownMenuRadioGroup value={value} onValueChange={setValue}>\n          <DropdownMenuRadioItem value=\"one\">Option One</DropdownMenuRadioItem>\n          <DropdownMenuRadioItem value=\"two\">Option Two</DropdownMenuRadioItem>\n          <DropdownMenuRadioItem value=\"three\">\n            Option Three\n          </DropdownMenuRadioItem>\n        </DropdownMenuRadioGroup>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n};\n\nexport const WithSubmenus = () => (\n  <DropdownMenu>\n    <DropdownMenuTrigger>\n      <Button>Open Menu</Button>\n    </DropdownMenuTrigger>\n    <DropdownMenuContent>\n      <DropdownMenuItem>Item One</DropdownMenuItem>\n      <DropdownMenuSub>\n        <DropdownMenuSubTrigger>More Options</DropdownMenuSubTrigger>\n        <DropdownMenuSubContent>\n          <DropdownMenuItem>Sub Item One</DropdownMenuItem>\n          <DropdownMenuItem>Sub Item Two</DropdownMenuItem>\n        </DropdownMenuSubContent>\n      </DropdownMenuSub>\n      <DropdownMenuItem>Item Three</DropdownMenuItem>\n    </DropdownMenuContent>\n  </DropdownMenu>\n);\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/dropdown/dropdown.tsx",
    "content": "'use client';\n\nimport * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';\nimport {\n  CheckIcon,\n  ChevronRightIcon,\n  DotFilledIcon,\n} from '@radix-ui/react-icons';\nimport * as React from 'react';\n\nimport { cn } from '@/utils/cn';\n\nconst DropdownMenu = DropdownMenuPrimitive.Root;\n\nconst DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;\n\nconst DropdownMenuGroup = DropdownMenuPrimitive.Group;\n\nconst DropdownMenuPortal = DropdownMenuPrimitive.Portal;\n\nconst DropdownMenuSub = DropdownMenuPrimitive.Sub;\n\nconst DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;\n\nconst DropdownMenuSubTrigger = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {\n    inset?: boolean;\n  }\n>(({ className, inset, children, ...props }, ref) => (\n  <DropdownMenuPrimitive.SubTrigger\n    ref={ref}\n    className={cn(\n      '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',\n      inset && 'pl-8',\n      className,\n    )}\n    {...props}\n  >\n    {children}\n    <ChevronRightIcon className=\"ml-auto size-4\" />\n  </DropdownMenuPrimitive.SubTrigger>\n));\nDropdownMenuSubTrigger.displayName =\n  DropdownMenuPrimitive.SubTrigger.displayName;\n\nconst DropdownMenuSubContent = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>\n>(({ className, ...props }, ref) => (\n  <DropdownMenuPrimitive.SubContent\n    ref={ref}\n    className={cn(\n      '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',\n      className,\n    )}\n    {...props}\n  />\n));\nDropdownMenuSubContent.displayName =\n  DropdownMenuPrimitive.SubContent.displayName;\n\nconst DropdownMenuContent = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>\n>(({ className, sideOffset = 4, ...props }, ref) => (\n  <DropdownMenuPrimitive.Portal>\n    <DropdownMenuPrimitive.Content\n      ref={ref}\n      sideOffset={sideOffset}\n      className={cn(\n        'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md',\n        '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',\n        className,\n      )}\n      {...props}\n    />\n  </DropdownMenuPrimitive.Portal>\n));\nDropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;\n\nconst DropdownMenuItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {\n    inset?: boolean;\n  }\n>(({ className, inset, ...props }, ref) => (\n  <DropdownMenuPrimitive.Item\n    ref={ref}\n    className={cn(\n      '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',\n      inset && 'pl-8',\n      className,\n    )}\n    {...props}\n  />\n));\nDropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;\n\nconst DropdownMenuCheckboxItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>\n>(({ className, children, checked, ...props }, ref) => (\n  <DropdownMenuPrimitive.CheckboxItem\n    ref={ref}\n    className={cn(\n      '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',\n      className,\n    )}\n    checked={checked}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex size-3.5 items-center justify-center\">\n      <DropdownMenuPrimitive.ItemIndicator>\n        <CheckIcon className=\"size-4\" />\n      </DropdownMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </DropdownMenuPrimitive.CheckboxItem>\n));\nDropdownMenuCheckboxItem.displayName =\n  DropdownMenuPrimitive.CheckboxItem.displayName;\n\nconst DropdownMenuRadioItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>\n>(({ className, children, ...props }, ref) => (\n  <DropdownMenuPrimitive.RadioItem\n    ref={ref}\n    className={cn(\n      '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',\n      className,\n    )}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex size-3.5 items-center justify-center\">\n      <DropdownMenuPrimitive.ItemIndicator>\n        <DotFilledIcon className=\"size-4 fill-current\" />\n      </DropdownMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </DropdownMenuPrimitive.RadioItem>\n));\nDropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;\n\nconst DropdownMenuLabel = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {\n    inset?: boolean;\n  }\n>(({ className, inset, ...props }, ref) => (\n  <DropdownMenuPrimitive.Label\n    ref={ref}\n    className={cn(\n      'px-2 py-1.5 text-sm font-semibold',\n      inset && 'pl-8',\n      className,\n    )}\n    {...props}\n  />\n));\nDropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;\n\nconst DropdownMenuSeparator = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <DropdownMenuPrimitive.Separator\n    ref={ref}\n    className={cn('-mx-1 my-1 h-px bg-muted', className)}\n    {...props}\n  />\n));\nDropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;\n\nconst DropdownMenuShortcut = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLSpanElement>) => {\n  return (\n    <span\n      className={cn('ml-auto text-xs tracking-widest opacity-60', className)}\n      {...props}\n    />\n  );\n};\nDropdownMenuShortcut.displayName = 'DropdownMenuShortcut';\n\nexport {\n  DropdownMenu,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuCheckboxItem,\n  DropdownMenuRadioItem,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuShortcut,\n  DropdownMenuGroup,\n  DropdownMenuPortal,\n  DropdownMenuSub,\n  DropdownMenuSubContent,\n  DropdownMenuSubTrigger,\n  DropdownMenuRadioGroup,\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/dropdown/index.ts",
    "content": "export * from './dropdown';\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/form/__tests__/form.test.tsx",
    "content": "import { SubmitHandler } from 'react-hook-form';\nimport { z } from 'zod';\n\nimport { Button } from '@/components/ui/button';\nimport { rtlRender, screen, waitFor, userEvent } from '@/testing/test-utils';\n\nimport { Form } from '../form';\nimport { Input } from '../input';\n\nconst testData = {\n  title: 'Hello World',\n};\n\nconst schema = z.object({\n  title: z.string().min(1, 'Required'),\n});\n\ntest('should render and submit a basic Form component', async () => {\n  const handleSubmit = vi.fn() as SubmitHandler<z.infer<typeof schema>>;\n\n  rtlRender(\n    <Form onSubmit={handleSubmit} schema={schema} id=\"my-form\">\n      {({ register, formState }) => (\n        <>\n          <Input\n            label=\"Title\"\n            error={formState.errors['title']}\n            registration={register('title')}\n          />\n\n          <Button name=\"submit\" type=\"submit\" className=\"w-full\">\n            Submit\n          </Button>\n        </>\n      )}\n    </Form>,\n  );\n\n  await userEvent.type(screen.getByLabelText(/title/i), testData.title);\n\n  await userEvent.click(screen.getByRole('button', { name: /submit/i }));\n\n  await waitFor(() =>\n    expect(handleSubmit).toHaveBeenCalledWith(testData, expect.anything()),\n  );\n});\n\ntest('should fail submission if validation fails', async () => {\n  const handleSubmit = vi.fn() as SubmitHandler<z.infer<typeof schema>>;\n\n  rtlRender(\n    <Form onSubmit={handleSubmit} schema={schema} id=\"my-form\">\n      {({ register, formState }) => (\n        <>\n          <Input\n            label=\"Title\"\n            error={formState.errors['title']}\n            registration={register('title')}\n          />\n\n          <Button name=\"submit\" type=\"submit\" className=\"w-full\">\n            Submit\n          </Button>\n        </>\n      )}\n    </Form>,\n  );\n\n  await userEvent.click(screen.getByRole('button', { name: /submit/i }));\n\n  await screen.findByRole('alert', { name: /required/i });\n\n  expect(handleSubmit).toHaveBeenCalledTimes(0);\n});\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/form/error.tsx",
    "content": "export type ErrorProps = {\n  errorMessage?: string | null;\n};\n\nexport const Error = ({ errorMessage }: ErrorProps) => {\n  if (!errorMessage) return null;\n\n  return (\n    <div\n      role=\"alert\"\n      aria-label={errorMessage}\n      className=\"text-sm font-semibold text-red-500\"\n    >\n      {errorMessage}\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/form/field-wrapper.tsx",
    "content": "import * as React from 'react';\nimport { type FieldError } from 'react-hook-form';\n\nimport { Error } from './error';\nimport { Label } from './label';\n\ntype FieldWrapperProps = {\n  label?: string;\n  className?: string;\n  children: React.ReactNode;\n  error?: FieldError | undefined;\n};\n\nexport type FieldWrapperPassThroughProps = Omit<\n  FieldWrapperProps,\n  'className' | 'children'\n>;\n\nexport const FieldWrapper = (props: FieldWrapperProps) => {\n  const { label, error, children } = props;\n  return (\n    <div>\n      <Label>\n        {label}\n        <div className=\"mt-1\">{children}</div>\n      </Label>\n      <Error errorMessage={error?.message} />\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/form/form-drawer.tsx",
    "content": "'use client';\n\nimport * as React from 'react';\n\nimport { useDisclosure } from '@/hooks/use-disclosure';\n\nimport { Button } from '../button';\nimport {\n  Drawer,\n  DrawerClose,\n  DrawerContent,\n  DrawerFooter,\n  DrawerHeader,\n  DrawerTrigger,\n  DrawerTitle,\n} from '../drawer';\n\ntype FormDrawerProps = {\n  isDone: boolean;\n  triggerButton: React.ReactElement;\n  submitButton: React.ReactElement;\n  title: string;\n  children: React.ReactNode;\n};\n\nexport const FormDrawer = ({\n  title,\n  children,\n  isDone,\n  triggerButton,\n  submitButton,\n}: FormDrawerProps) => {\n  const { close, open, isOpen } = useDisclosure();\n\n  React.useEffect(() => {\n    if (isDone) {\n      close();\n    }\n  }, [isDone, close]);\n\n  return (\n    <Drawer\n      open={isOpen}\n      onOpenChange={(isOpen) => {\n        if (!isOpen) {\n          close();\n        } else {\n          open();\n        }\n      }}\n    >\n      <DrawerTrigger asChild>{triggerButton}</DrawerTrigger>\n      <DrawerContent className=\"flex max-w-[800px] flex-col justify-between sm:max-w-[540px]\">\n        <div className=\"flex flex-col\">\n          <DrawerHeader>\n            <DrawerTitle>{title}</DrawerTitle>\n          </DrawerHeader>\n          <div>{children}</div>\n        </div>\n        <DrawerFooter>\n          <DrawerClose asChild>\n            <Button variant=\"outline\" type=\"submit\">\n              Close\n            </Button>\n          </DrawerClose>\n          {submitButton}\n        </DrawerFooter>\n      </DrawerContent>\n    </Drawer>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/form/form.stories.tsx",
    "content": "import { Meta, StoryObj } from '@storybook/react';\nimport { z } from 'zod';\n\nimport { Button } from '../button';\n\nimport { Form } from './form';\nimport { FormDrawer } from './form-drawer';\nimport { Input } from './input';\nimport { Select } from './select';\nimport { Textarea } from './textarea';\n\nconst MyForm = ({ hideSubmit = false }: { hideSubmit?: boolean }) => {\n  return (\n    <Form\n      onSubmit={async (values) => {\n        alert(JSON.stringify(values, null, 2));\n      }}\n      schema={z.object({\n        title: z.string().min(1, 'Required'),\n        description: z.string().min(1, 'Required'),\n        type: z.string().min(1, 'Required'),\n      })}\n      id=\"my-form\"\n    >\n      {({ register, formState }) => (\n        <>\n          <Input\n            label=\"Title\"\n            error={formState.errors['title']}\n            registration={register('title')}\n          />\n          <Textarea\n            label=\"Description\"\n            error={formState.errors['description']}\n            registration={register('description')}\n          />\n          <Select\n            label=\"Type\"\n            error={formState.errors['type']}\n            registration={register('type')}\n            options={['A', 'B', 'C'].map((type) => ({\n              label: type,\n              value: type,\n            }))}\n          />\n\n          {!hideSubmit && (\n            <div>\n              <Button type=\"submit\" className=\"w-full\">\n                Submit\n              </Button>\n            </div>\n          )}\n        </>\n      )}\n    </Form>\n  );\n};\n\nconst meta: Meta = {\n  component: MyForm,\n};\n\nexport default meta;\n\ntype Story = StoryObj<typeof MyForm>;\n\nexport const Default: Story = {\n  render: () => <MyForm />,\n};\n\nexport const AsFormDrawer: Story = {\n  render: () => (\n    <FormDrawer\n      triggerButton={<Button>Open Form</Button>}\n      isDone={true}\n      title=\"My Form\"\n      submitButton={\n        <Button form=\"my-form\" type=\"submit\">\n          Submit\n        </Button>\n      }\n    >\n      <MyForm hideSubmit />\n    </FormDrawer>\n  ),\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/form/form.tsx",
    "content": "'use client';\n\nimport { zodResolver } from '@hookform/resolvers/zod';\nimport * as LabelPrimitive from '@radix-ui/react-label';\nimport { Slot } from '@radix-ui/react-slot';\nimport * as React from 'react';\nimport {\n  Controller,\n  ControllerProps,\n  FieldPath,\n  FieldValues,\n  FormProvider,\n  SubmitHandler,\n  UseFormProps,\n  UseFormReturn,\n  useForm,\n  useFormContext,\n} from 'react-hook-form';\nimport { ZodType, z } from 'zod';\n\nimport { cn } from '@/utils/cn';\n\nimport { Label } from './label';\n\ntype FormFieldContextValue<\n  TFieldValues extends FieldValues = FieldValues,\n  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n> = {\n  name: TName;\n};\n\nconst FormFieldContext = React.createContext<FormFieldContextValue>(\n  {} as FormFieldContextValue,\n);\n\nconst FormField = <\n  TFieldValues extends FieldValues,\n  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n>({\n  ...props\n}: ControllerProps<TFieldValues, TName>) => {\n  return (\n    <FormFieldContext.Provider value={{ name: props.name }}>\n      <Controller {...props} />\n    </FormFieldContext.Provider>\n  );\n};\n\nconst useFormField = () => {\n  const fieldContext = React.useContext(FormFieldContext);\n  const itemContext = React.useContext(FormItemContext);\n  const { getFieldState, formState } = useFormContext();\n\n  const fieldState = getFieldState(fieldContext.name, formState);\n\n  if (!fieldContext) {\n    throw new Error('useFormField should be used within <FormField>');\n  }\n\n  const { id } = itemContext;\n\n  return {\n    id,\n    name: fieldContext.name,\n    formItemId: `${id}-form-item`,\n    formDescriptionId: `${id}-form-item-description`,\n    formMessageId: `${id}-form-item-message`,\n    ...fieldState,\n  };\n};\n\ntype FormItemContextValue = {\n  id: string;\n};\n\nconst FormItemContext = React.createContext<FormItemContextValue>(\n  {} as FormItemContextValue,\n);\n\nconst FormItem = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => {\n  const id = React.useId();\n\n  return (\n    <FormItemContext.Provider value={{ id }}>\n      <div ref={ref} className={cn('space-y-2', className)} {...props} />\n    </FormItemContext.Provider>\n  );\n});\nFormItem.displayName = 'FormItem';\n\nconst FormLabel = React.forwardRef<\n  React.ElementRef<typeof LabelPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>\n>(({ className, ...props }, ref) => {\n  const { error, formItemId } = useFormField();\n\n  return (\n    <Label\n      ref={ref}\n      className={cn(error && 'text-destructive', className)}\n      htmlFor={formItemId}\n      {...props}\n    />\n  );\n});\nFormLabel.displayName = 'FormLabel';\n\nconst FormControl = React.forwardRef<\n  React.ElementRef<typeof Slot>,\n  React.ComponentPropsWithoutRef<typeof Slot>\n>(({ ...props }, ref) => {\n  const { error, formItemId, formDescriptionId, formMessageId } =\n    useFormField();\n\n  return (\n    <Slot\n      ref={ref}\n      id={formItemId}\n      aria-describedby={\n        !error\n          ? `${formDescriptionId}`\n          : `${formDescriptionId} ${formMessageId}`\n      }\n      aria-invalid={!!error}\n      {...props}\n    />\n  );\n});\nFormControl.displayName = 'FormControl';\n\nconst FormDescription = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, ...props }, ref) => {\n  const { formDescriptionId } = useFormField();\n\n  return (\n    <p\n      ref={ref}\n      id={formDescriptionId}\n      className={cn('text-[0.8rem] text-muted-foreground', className)}\n      {...props}\n    />\n  );\n});\nFormDescription.displayName = 'FormDescription';\n\nconst FormMessage = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, children, ...props }, ref) => {\n  const { error, formMessageId } = useFormField();\n  const body = error ? String(error?.message) : children;\n\n  if (!body) {\n    return null;\n  }\n\n  return (\n    <p\n      ref={ref}\n      id={formMessageId}\n      className={cn('text-[0.8rem] font-medium text-destructive', className)}\n      {...props}\n    >\n      {body}\n    </p>\n  );\n});\nFormMessage.displayName = 'FormMessage';\n\ntype FormProps<TFormValues extends FieldValues, Schema> = {\n  onSubmit: SubmitHandler<TFormValues>;\n  schema: Schema;\n  className?: string;\n  children: (methods: UseFormReturn<TFormValues>) => React.ReactNode;\n  options?: UseFormProps<TFormValues>;\n  id?: string;\n};\n\nconst Form = <\n  Schema extends ZodType<any, any, any>,\n  TFormValues extends FieldValues = z.infer<Schema>,\n>({\n  onSubmit,\n  children,\n  className,\n  options,\n  id,\n  schema,\n}: FormProps<TFormValues, Schema>) => {\n  const form = useForm({ ...options, resolver: zodResolver(schema) });\n  return (\n    <FormProvider {...form}>\n      <form\n        className={cn('space-y-6', className)}\n        onSubmit={form.handleSubmit(onSubmit)}\n        id={id}\n      >\n        {children(form)}\n      </form>\n    </FormProvider>\n  );\n};\n\nexport {\n  useFormField,\n  Form,\n  FormProvider,\n  FormItem,\n  FormLabel,\n  FormControl,\n  FormDescription,\n  FormMessage,\n  FormField,\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/form/index.ts",
    "content": "export * from './form';\nexport * from './input';\nexport * from './select';\nexport * from './textarea';\nexport * from './form-drawer';\nexport * from './label';\nexport * from './switch';\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/form/input.tsx",
    "content": "import * as React from 'react';\nimport { type UseFormRegisterReturn } from 'react-hook-form';\n\nimport { cn } from '@/utils/cn';\n\nimport { FieldWrapper, FieldWrapperPassThroughProps } from './field-wrapper';\n\nexport type InputProps = React.InputHTMLAttributes<HTMLInputElement> &\n  FieldWrapperPassThroughProps & {\n    className?: string;\n    registration: Partial<UseFormRegisterReturn>;\n  };\n\nconst Input = React.forwardRef<HTMLInputElement, InputProps>(\n  ({ className, type, label, error, registration, ...props }, ref) => {\n    return (\n      <FieldWrapper label={label} error={error}>\n        <input\n          type={type}\n          className={cn(\n            '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',\n            className,\n          )}\n          ref={ref}\n          {...registration}\n          {...props}\n        />\n      </FieldWrapper>\n    );\n  },\n);\nInput.displayName = 'Input';\n\nexport { Input };\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/form/label.tsx",
    "content": "'use client';\n\nimport * as LabelPrimitive from '@radix-ui/react-label';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport * as React from 'react';\n\nimport { cn } from '@/utils/cn';\n\nconst labelVariants = cva(\n  'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',\n);\n\nconst Label = React.forwardRef<\n  React.ElementRef<typeof LabelPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &\n    VariantProps<typeof labelVariants>\n>(({ className, ...props }, ref) => (\n  <LabelPrimitive.Root\n    ref={ref}\n    className={cn(labelVariants(), className)}\n    {...props}\n  />\n));\nLabel.displayName = LabelPrimitive.Root.displayName;\n\nexport { Label };\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/form/select.tsx",
    "content": "'use client';\n\nimport * as React from 'react';\nimport { UseFormRegisterReturn } from 'react-hook-form';\n\nimport { cn } from '@/utils/cn';\n\nimport { FieldWrapper, FieldWrapperPassThroughProps } from './field-wrapper';\n\ntype Option = {\n  label: React.ReactNode;\n  value: string | number | string[];\n};\n\ntype SelectFieldProps = FieldWrapperPassThroughProps & {\n  options: Option[];\n  className?: string;\n  defaultValue?: string;\n  registration: Partial<UseFormRegisterReturn>;\n};\n\nexport const Select = (props: SelectFieldProps) => {\n  const { label, options, error, className, defaultValue, registration } =\n    props;\n  return (\n    <FieldWrapper label={label} error={error}>\n      <select\n        className={cn(\n          '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',\n          className,\n        )}\n        defaultValue={defaultValue}\n        {...registration}\n      >\n        {options.map(({ label, value }) => (\n          <option key={label?.toString()} value={value}>\n            {label}\n          </option>\n        ))}\n      </select>\n    </FieldWrapper>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/form/switch.tsx",
    "content": "'use client';\n\nimport * as SwitchPrimitives from '@radix-ui/react-switch';\nimport * as React from 'react';\n\nimport { cn } from '@/utils/cn';\n\nconst Switch = React.forwardRef<\n  React.ElementRef<typeof SwitchPrimitives.Root>,\n  React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>\n>(({ className, ...props }, ref) => (\n  <SwitchPrimitives.Root\n    className={cn(\n      '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',\n      className,\n    )}\n    {...props}\n    ref={ref}\n  >\n    <SwitchPrimitives.Thumb\n      className={cn(\n        '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',\n      )}\n    />\n  </SwitchPrimitives.Root>\n));\nSwitch.displayName = SwitchPrimitives.Root.displayName;\n\nexport { Switch };\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/form/textarea.tsx",
    "content": "import * as React from 'react';\nimport { UseFormRegisterReturn } from 'react-hook-form';\n\nimport { cn } from '@/utils/cn';\n\nimport { FieldWrapper, FieldWrapperPassThroughProps } from './field-wrapper';\n\nexport type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement> &\n  FieldWrapperPassThroughProps & {\n    className?: string;\n    registration: Partial<UseFormRegisterReturn>;\n  };\n\nconst Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(\n  ({ className, label, error, registration, ...props }, ref) => {\n    return (\n      <FieldWrapper label={label} error={error}>\n        <textarea\n          className={cn(\n            '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',\n            className,\n          )}\n          ref={ref}\n          {...registration}\n          {...props}\n        />\n      </FieldWrapper>\n    );\n  },\n);\nTextarea.displayName = 'Textarea';\n\nexport { Textarea };\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/link/index.ts",
    "content": "export * from './link';\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/link/link.stories.tsx",
    "content": "import { Meta, StoryObj } from '@storybook/react';\n\nimport { Link } from './link';\n\nconst meta: Meta<typeof Link> = {\n  component: Link,\n};\n\nexport default meta;\n\ntype Story = StoryObj<typeof Link>;\n\nexport const Default: Story = {\n  args: {\n    children: 'Link',\n    href: '/',\n  },\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/link/link.tsx",
    "content": "import NextLink, { LinkProps as NextLinkProps } from 'next/link';\n\nimport { cn } from '@/utils/cn';\n\nexport type LinkProps = {\n  className?: string;\n  children: React.ReactNode;\n  target?: string;\n} & NextLinkProps;\n\nexport const Link = ({ className, children, href, ...props }: LinkProps) => {\n  return (\n    <NextLink\n      href={href}\n      className={cn('text-slate-600 hover:text-slate-900', className)}\n      {...props}\n    >\n      {children}\n    </NextLink>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/md-preview/index.ts",
    "content": "export * from './md-preview';\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/md-preview/md-preview.stories.tsx",
    "content": "import { Meta, StoryObj } from '@storybook/react';\n\nimport { MDPreview } from './md-preview';\n\nconst meta: Meta<typeof MDPreview> = {\n  component: MDPreview,\n};\n\nexport default meta;\n\ntype Story = StoryObj<typeof MDPreview>;\n\nexport const Default: Story = {\n  args: {\n    value: `## Hello World!`,\n  },\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/md-preview/md-preview.tsx",
    "content": "import DOMPurify from 'isomorphic-dompurify';\nimport { parse } from 'marked';\n\nexport type MDPreviewProps = {\n  value: string;\n};\n\nexport const MDPreview = ({ value = '' }: MDPreviewProps) => {\n  return (\n    <div\n      className=\"prose prose-slate w-full p-2\"\n      dangerouslySetInnerHTML={{\n        __html: DOMPurify.sanitize(parse(value) as string),\n      }}\n    />\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/notifications/__tests__/notifications.test.ts",
    "content": "import { renderHook, act } from '@testing-library/react';\n\nimport { useNotifications, Notification } from '../notifications-store';\n\ntest('should add and remove notifications', () => {\n  const { result } = renderHook(() => useNotifications());\n\n  expect(result.current.notifications.length).toBe(0);\n\n  const notification: Notification = {\n    id: '123',\n    title: 'Hello World',\n    type: 'info',\n    message: 'This is a notification',\n  };\n\n  act(() => {\n    result.current.addNotification(notification);\n  });\n\n  expect(result.current.notifications).toContainEqual(notification);\n\n  act(() => {\n    result.current.dismissNotification(notification.id);\n  });\n\n  expect(result.current.notifications).not.toContainEqual(notification);\n});\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/notifications/index.ts",
    "content": "export * from './notifications';\nexport * from './notifications-store';\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/notifications/notification.stories.tsx",
    "content": "import { Meta, StoryObj } from '@storybook/react';\n\nimport { Notification } from './notification';\n\nconst meta: Meta<typeof Notification> = {\n  title: 'Components/Notifications',\n  component: Notification,\n  parameters: {\n    controls: { expanded: true },\n  },\n};\n\nexport default meta;\n\ntype Story = StoryObj<typeof Notification>;\n\nexport const Info: Story = {\n  args: {\n    notification: {\n      id: '1',\n      type: 'info',\n      title: 'Hello Info',\n      message: 'This is info notification',\n    },\n    onDismiss: (id) => alert(`Dismissing Notification with id: ${id}`),\n  },\n};\n\nexport const Success: Story = {\n  args: {\n    notification: {\n      id: '1',\n      type: 'success',\n      title: 'Hello Success',\n      message: 'This is success notification',\n    },\n    onDismiss: (id) => alert(`Dismissing Notification with id: ${id}`),\n  },\n};\n\nexport const Warning: Story = {\n  args: {\n    notification: {\n      id: '1',\n      type: 'warning',\n      title: 'Hello Warning',\n      message: 'This is warning notification',\n    },\n    onDismiss: (id) => alert(`Dismissing Notification with id: ${id}`),\n  },\n};\n\nexport const Error: Story = {\n  args: {\n    notification: {\n      id: '1',\n      type: 'error',\n      title: 'Hello Error',\n      message: 'This is error notification',\n    },\n    onDismiss: (id) => alert(`Dismissing Notification with id: ${id}`),\n  },\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/notifications/notification.tsx",
    "content": "'use client';\n\nimport { Info, CircleAlert, CircleX, CircleCheck } from 'lucide-react';\n\nconst icons = {\n  info: <Info className=\"size-6 text-blue-500\" aria-hidden=\"true\" />,\n  success: <CircleCheck className=\"size-6 text-green-500\" aria-hidden=\"true\" />,\n  warning: (\n    <CircleAlert className=\"size-6 text-yellow-500\" aria-hidden=\"true\" />\n  ),\n  error: <CircleX className=\"size-6 text-red-500\" aria-hidden=\"true\" />,\n};\n\nexport type NotificationProps = {\n  notification: {\n    id: string;\n    type: keyof typeof icons;\n    title: string;\n    message?: string;\n  };\n  onDismiss: (id: string) => void;\n};\n\nexport const Notification = ({\n  notification: { id, type, title, message },\n  onDismiss,\n}: NotificationProps) => {\n  return (\n    <div className=\"flex w-full flex-col items-center space-y-4 sm:items-end\">\n      <div className=\"pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg bg-white shadow-lg ring-1 ring-black/5\">\n        <div className=\"p-4\" role=\"alert\" aria-label={title}>\n          <div className=\"flex items-start\">\n            <div className=\"shrink-0\">{icons[type]}</div>\n            <div className=\"ml-3 w-0 flex-1 pt-0.5\">\n              <p className=\"text-sm font-medium text-gray-900\">{title}</p>\n              <p className=\"mt-1 text-sm text-gray-500\">{message}</p>\n            </div>\n            <div className=\"ml-4 flex shrink-0\">\n              <button\n                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\"\n                onClick={() => {\n                  onDismiss(id);\n                }}\n              >\n                <span className=\"sr-only\">Close</span>\n                <CircleX className=\"size-5\" aria-hidden=\"true\" />\n              </button>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/notifications/notifications-store.ts",
    "content": "import { nanoid } from 'nanoid';\nimport { create } from 'zustand';\n\nexport type Notification = {\n  id: string;\n  type: 'info' | 'warning' | 'success' | 'error';\n  title: string;\n  message?: string;\n};\n\ntype NotificationsStore = {\n  notifications: Notification[];\n  addNotification: (notification: Omit<Notification, 'id'>) => void;\n  dismissNotification: (id: string) => void;\n};\n\nexport const useNotifications = create<NotificationsStore>((set) => ({\n  notifications: [],\n  addNotification: (notification) =>\n    set((state) => ({\n      notifications: [\n        ...state.notifications,\n        { id: nanoid(), ...notification },\n      ],\n    })),\n  dismissNotification: (id) =>\n    set((state) => ({\n      notifications: state.notifications.filter(\n        (notification) => notification.id !== id,\n      ),\n    })),\n}));\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/notifications/notifications.tsx",
    "content": "'use client';\n\nimport { Notification } from './notification';\nimport { useNotifications } from './notifications-store';\n\nexport const Notifications = () => {\n  const { notifications, dismissNotification } = useNotifications();\n\n  return (\n    <div\n      aria-live=\"assertive\"\n      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\"\n    >\n      {notifications.map((notification) => (\n        <Notification\n          key={notification.id}\n          notification={notification}\n          onDismiss={dismissNotification}\n        />\n      ))}\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/spinner/index.ts",
    "content": "export * from './spinner';\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/spinner/spinner.stories.tsx",
    "content": "import { Meta, StoryObj } from '@storybook/react';\n\nimport { Spinner } from './spinner';\n\nconst meta: Meta<typeof Spinner> = {\n  component: Spinner,\n};\n\nexport default meta;\n\ntype Story = StoryObj<typeof Spinner>;\n\nexport const Default: Story = {\n  args: {\n    size: 'md',\n  },\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/spinner/spinner.tsx",
    "content": "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',\n};\n\nconst variants = {\n  light: 'text-white',\n  primary: 'text-slate-600',\n};\n\nexport type SpinnerProps = {\n  size?: keyof typeof sizes;\n  variant?: keyof typeof variants;\n  className?: string;\n};\n\nexport const Spinner = ({\n  size = 'md',\n  variant = 'primary',\n  className = '',\n}: SpinnerProps) => {\n  return (\n    <>\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        width=\"24\"\n        height=\"24\"\n        viewBox=\"0 0 24 24\"\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeWidth=\"2\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        className={cn(\n          'animate-spin',\n          sizes[size],\n          variants[variant],\n          className,\n        )}\n      >\n        <path d=\"M21 12a9 9 0 1 1-6.219-8.56\" />\n      </svg>\n      <span className=\"sr-only\">Loading</span>\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/table/index.ts",
    "content": "export * from './table';\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/table/pagination.tsx",
    "content": "import {\n  ChevronLeftIcon,\n  ChevronRightIcon,\n  DotsHorizontalIcon,\n} from '@radix-ui/react-icons';\nimport * as React from 'react';\n\nimport { ButtonProps, buttonVariants } from '@/components/ui/button';\nimport { cn } from '@/utils/cn';\n\nimport { Link } from '../link';\n\nconst Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => (\n  <nav\n    role=\"navigation\"\n    aria-label=\"pagination\"\n    className={cn('mx-auto flex w-full justify-center', className)}\n    {...props}\n  />\n);\nPagination.displayName = 'Pagination';\n\nconst PaginationContent = React.forwardRef<\n  HTMLUListElement,\n  React.ComponentProps<'ul'>\n>(({ className, ...props }, ref) => (\n  <ul\n    ref={ref}\n    className={cn('flex flex-row items-center gap-1', className)}\n    {...props}\n  />\n));\nPaginationContent.displayName = 'PaginationContent';\n\nconst PaginationItem = React.forwardRef<\n  HTMLLIElement,\n  React.ComponentProps<'li'>\n>(({ className, ...props }, ref) => (\n  <li ref={ref} className={cn('', className)} {...props} />\n));\nPaginationItem.displayName = 'PaginationItem';\n\ntype PaginationLinkProps = {\n  isActive?: boolean;\n} & Pick<ButtonProps, 'size'> &\n  React.ComponentProps<'a'>;\n\nconst PaginationLink = ({\n  className,\n  isActive,\n  size = 'icon',\n  children,\n  href,\n  ...props\n}: PaginationLinkProps) => (\n  <Link\n    href={href as string}\n    aria-current={isActive ? 'page' : undefined}\n    className={cn(\n      buttonVariants({\n        variant: isActive ? 'outline' : 'ghost',\n        size,\n      }),\n      className,\n    )}\n    {...props}\n  >\n    {children}\n  </Link>\n);\nPaginationLink.displayName = 'PaginationLink';\n\nconst PaginationPrevious = ({\n  className,\n  ...props\n}: React.ComponentProps<typeof PaginationLink>) => (\n  <PaginationLink\n    aria-label=\"Go to previous page\"\n    size=\"default\"\n    className={cn('gap-1 pl-2.5', className)}\n    {...props}\n  >\n    <ChevronLeftIcon className=\"size-4\" />\n    <span>Previous</span>\n  </PaginationLink>\n);\nPaginationPrevious.displayName = 'PaginationPrevious';\n\nconst PaginationNext = ({\n  className,\n  ...props\n}: React.ComponentProps<typeof PaginationLink>) => (\n  <PaginationLink\n    aria-label=\"Go to next page\"\n    size=\"default\"\n    className={cn('gap-1 pr-2.5', className)}\n    {...props}\n  >\n    <span>Next</span>\n    <ChevronRightIcon className=\"size-4\" />\n  </PaginationLink>\n);\nPaginationNext.displayName = 'PaginationNext';\n\nconst PaginationEllipsis = ({\n  className,\n  ...props\n}: React.ComponentProps<'span'>) => (\n  <span\n    aria-hidden\n    className={cn('flex h-9 w-9 items-center justify-center', className)}\n    {...props}\n  >\n    <DotsHorizontalIcon className=\"size-4\" />\n    <span className=\"sr-only\">More pages</span>\n  </span>\n);\nPaginationEllipsis.displayName = 'PaginationEllipsis';\n\nexport {\n  Pagination,\n  PaginationContent,\n  PaginationLink,\n  PaginationItem,\n  PaginationPrevious,\n  PaginationNext,\n  PaginationEllipsis,\n};\n\nexport type TablePaginationProps = {\n  totalPages: number;\n  currentPage: number;\n  rootUrl: string;\n};\n\nexport const TablePagination = ({\n  totalPages,\n  currentPage,\n  rootUrl,\n}: TablePaginationProps) => {\n  const createHref = (page: number) => `${rootUrl}?page=${page}`;\n\n  return (\n    <Pagination className=\"justify-end py-8\">\n      <PaginationContent>\n        {currentPage > 1 && (\n          <PaginationItem>\n            <PaginationPrevious href={createHref(currentPage - 1)} />\n          </PaginationItem>\n        )}\n        {currentPage > 2 && (\n          <PaginationItem>\n            <PaginationEllipsis />\n          </PaginationItem>\n        )}\n        {currentPage > 1 && (\n          <PaginationItem>\n            <PaginationLink href={createHref(currentPage - 1)}>\n              {currentPage - 1}\n            </PaginationLink>\n          </PaginationItem>\n        )}\n        <PaginationItem className=\"rounded-sm bg-gray-200\">\n          <PaginationLink href={createHref(currentPage)}>\n            {currentPage}\n          </PaginationLink>\n        </PaginationItem>\n        {totalPages > currentPage && (\n          <PaginationItem>\n            <PaginationLink href={createHref(currentPage + 1)}>\n              {currentPage + 1}\n            </PaginationLink>\n          </PaginationItem>\n        )}\n        {totalPages > currentPage + 1 && (\n          <PaginationItem>\n            <PaginationEllipsis />\n          </PaginationItem>\n        )}\n        {currentPage < totalPages && (\n          <PaginationItem>\n            <PaginationNext href={createHref(totalPages)} />\n          </PaginationItem>\n        )}\n      </PaginationContent>\n    </Pagination>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/table/table.stories.tsx",
    "content": "import { Meta, StoryObj } from '@storybook/react';\n\nimport { Table } from './table';\n\nconst meta: Meta<typeof Table> = {\n  component: Table,\n};\n\nexport default meta;\n\ntype User = {\n  id: string;\n  createdAt: number;\n  name: string;\n  title: string;\n  role: string;\n  email: string;\n};\n\ntype Story = StoryObj<typeof Table<User>>;\n\nconst data: User[] = [\n  {\n    id: '1',\n    createdAt: Date.now(),\n    name: 'Jane Cooper',\n    title: 'Regional Paradigm Technician',\n    role: 'Admin',\n    email: 'jane.cooper@example.com',\n  },\n  {\n    id: '2',\n    createdAt: Date.now(),\n    name: 'Cody Fisher',\n    title: 'Product Directives Officer',\n    role: 'Owner',\n    email: 'cody.fisher@example.com',\n  },\n];\n\nexport const Default: Story = {\n  args: {\n    data,\n    columns: [\n      {\n        title: 'Name',\n        field: 'name',\n      },\n      {\n        title: 'Title',\n        field: 'title',\n      },\n      {\n        title: 'Role',\n        field: 'role',\n      },\n      {\n        title: 'Email',\n        field: 'email',\n      },\n    ],\n  },\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/components/ui/table/table.tsx",
    "content": "import { ArchiveX } from 'lucide-react';\nimport * as React from 'react';\n\nimport { BaseEntity } from '@/types/api';\nimport { cn } from '@/utils/cn';\n\nimport { TablePagination, TablePaginationProps } from './pagination';\n\nconst TableElement = React.forwardRef<\n  HTMLTableElement,\n  React.HTMLAttributes<HTMLTableElement>\n>(({ className, ...props }, ref) => (\n  <div className=\"relative w-full overflow-auto\">\n    <table\n      ref={ref}\n      className={cn('w-full caption-bottom text-sm', className)}\n      {...props}\n    />\n  </div>\n));\nTableElement.displayName = 'Table';\n\nconst TableHeader = React.forwardRef<\n  HTMLTableSectionElement,\n  React.HTMLAttributes<HTMLTableSectionElement>\n>(({ className, ...props }, ref) => (\n  <thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />\n));\nTableHeader.displayName = 'TableHeader';\n\nconst TableBody = React.forwardRef<\n  HTMLTableSectionElement,\n  React.HTMLAttributes<HTMLTableSectionElement>\n>(({ className, ...props }, ref) => (\n  <tbody\n    ref={ref}\n    className={cn('[&_tr:last-child]:border-0', className)}\n    {...props}\n  />\n));\nTableBody.displayName = 'TableBody';\n\nconst TableFooter = React.forwardRef<\n  HTMLTableSectionElement,\n  React.HTMLAttributes<HTMLTableSectionElement>\n>(({ className, ...props }, ref) => (\n  <tfoot\n    ref={ref}\n    className={cn(\n      'border-t bg-muted/50 font-medium [&>tr]:last:border-b-0',\n      className,\n    )}\n    {...props}\n  />\n));\nTableFooter.displayName = 'TableFooter';\n\nconst TableRow = React.forwardRef<\n  HTMLTableRowElement,\n  React.HTMLAttributes<HTMLTableRowElement>\n>(({ className, ...props }, ref) => (\n  <tr\n    ref={ref}\n    className={cn(\n      'border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted',\n      className,\n    )}\n    {...props}\n  />\n));\nTableRow.displayName = 'TableRow';\n\nconst TableHead = React.forwardRef<\n  HTMLTableCellElement,\n  React.ThHTMLAttributes<HTMLTableCellElement>\n>(({ className, ...props }, ref) => (\n  <th\n    ref={ref}\n    className={cn(\n      'h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',\n      className,\n    )}\n    {...props}\n  />\n));\nTableHead.displayName = 'TableHead';\n\nconst TableCell = React.forwardRef<\n  HTMLTableCellElement,\n  React.TdHTMLAttributes<HTMLTableCellElement>\n>(({ className, ...props }, ref) => (\n  <td\n    ref={ref}\n    className={cn(\n      'p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',\n      className,\n    )}\n    {...props}\n  />\n));\nTableCell.displayName = 'TableCell';\n\nconst TableCaption = React.forwardRef<\n  HTMLTableCaptionElement,\n  React.HTMLAttributes<HTMLTableCaptionElement>\n>(({ className, ...props }, ref) => (\n  <caption\n    ref={ref}\n    className={cn('mt-4 text-sm text-muted-foreground', className)}\n    {...props}\n  />\n));\nTableCaption.displayName = 'TableCaption';\n\nexport {\n  TableElement,\n  TableHeader,\n  TableBody,\n  TableFooter,\n  TableHead,\n  TableRow,\n  TableCell,\n  TableCaption,\n};\n\ntype TableColumn<Entry> = {\n  title: string;\n  field: keyof Entry;\n  Cell?({ entry }: { entry: Entry }): React.ReactElement;\n};\n\nexport type TableProps<Entry> = {\n  data: Entry[];\n  columns: TableColumn<Entry>[];\n  pagination?: TablePaginationProps;\n};\n\nexport const Table = <Entry extends BaseEntity>({\n  data,\n  columns,\n  pagination,\n}: TableProps<Entry>) => {\n  if (!data?.length) {\n    return (\n      <div className=\"flex h-80 flex-col items-center justify-center bg-white text-gray-500\">\n        <ArchiveX className=\"size-16\" />\n        <h4>No Entries Found</h4>\n      </div>\n    );\n  }\n  return (\n    <>\n      <TableElement>\n        <TableHeader>\n          <TableRow>\n            {columns.map((column, index) => (\n              <TableHead key={column.title + index}>{column.title}</TableHead>\n            ))}\n          </TableRow>\n        </TableHeader>\n        <TableBody>\n          {data.map((entry, entryIndex) => (\n            <TableRow key={entry?.id || entryIndex}>\n              {columns.map(({ Cell, field, title }, columnIndex) => (\n                <TableCell key={title + columnIndex}>\n                  {Cell ? <Cell entry={entry} /> : `${entry[field]}`}\n                </TableCell>\n              ))}\n            </TableRow>\n          ))}\n        </TableBody>\n      </TableElement>\n\n      {pagination && <TablePagination {...pagination} />}\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/config/env.ts",
    "content": "import * as z from 'zod';\nimport 'dotenv/config';\n\nconst createEnv = () => {\n  const EnvSchema = z.object({\n    API_URL: z.string(),\n    ENABLE_API_MOCKING: z\n      .string()\n      .refine((s) => s === 'true' || s === 'false')\n      .transform((s) => s === 'true')\n      .optional(),\n    APP_URL: z.string().optional().default('http://localhost:3000'),\n    APP_MOCK_API_PORT: z.string().optional().default('8080'),\n  });\n\n  const envVars = {\n    API_URL: process.env.NEXT_PUBLIC_API_URL,\n    ENABLE_API_MOCKING: process.env.NEXT_PUBLIC_ENABLE_API_MOCKING,\n    APP_URL: process.env.NEXT_PUBLIC_URL,\n    APP_MOCK_API_PORT: process.env.NEXT_PUBLIC_MOCK_API_PORT,\n  };\n\n  const parsedEnv = EnvSchema.safeParse(envVars);\n\n  if (!parsedEnv.success) {\n    throw new Error(\n      `Invalid env provided.\n  The following variables are missing or invalid:\n  ${Object.entries(parsedEnv.error.flatten().fieldErrors)\n    .map(([k, v]) => `- ${k}: ${v}`)\n    .join('\\n')}\n  `,\n    );\n  }\n\n  return parsedEnv.data ?? {};\n};\n\nexport const env = createEnv();\n"
  },
  {
    "path": "apps/nextjs-app/src/config/paths.ts",
    "content": "export const paths = {\n  home: {\n    getHref: () => '/',\n  },\n\n  auth: {\n    register: {\n      getHref: (redirectTo?: string | null | undefined) =>\n        `/auth/register${redirectTo ? `?redirectTo=${encodeURIComponent(redirectTo)}` : ''}`,\n    },\n    login: {\n      getHref: (redirectTo?: string | null | undefined) =>\n        `/auth/login${redirectTo ? `?redirectTo=${encodeURIComponent(redirectTo)}` : ''}`,\n    },\n  },\n\n  app: {\n    root: {\n      getHref: () => '/app',\n    },\n    dashboard: {\n      getHref: () => '/app',\n    },\n    discussions: {\n      getHref: () => '/app/discussions',\n    },\n    discussion: {\n      getHref: (id: string) => `/app/discussions/${id}`,\n    },\n    users: {\n      getHref: () => '/app/users',\n    },\n    profile: {\n      getHref: () => '/app/profile',\n    },\n  },\n  public: {\n    discussion: {\n      getHref: (id: string) => `/public/discussions/${id}`,\n    },\n  },\n} as const;\n"
  },
  {
    "path": "apps/nextjs-app/src/features/auth/components/__tests__/login-form.test.tsx",
    "content": "import {\n  createUser,\n  renderApp,\n  screen,\n  userEvent,\n  waitFor,\n} from '@/testing/test-utils';\n\nimport { LoginForm } from '../login-form';\n\ntest('should login new user and call onSuccess cb which should navigate the user to the app', async () => {\n  const newUser = await createUser({ teamId: undefined });\n\n  const onSuccess = vi.fn();\n\n  await renderApp(<LoginForm onSuccess={onSuccess} />, { user: null });\n\n  await userEvent.type(screen.getByLabelText(/email address/i), newUser.email);\n  await userEvent.type(screen.getByLabelText(/password/i), newUser.password);\n\n  await userEvent.click(screen.getByRole('button', { name: /log in/i }));\n\n  await waitFor(() => expect(onSuccess).toHaveBeenCalledTimes(1));\n});\n"
  },
  {
    "path": "apps/nextjs-app/src/features/auth/components/__tests__/register-form.test.tsx",
    "content": "import { createUser } from '@/testing/data-generators';\nimport { renderApp, screen, userEvent, waitFor } from '@/testing/test-utils';\n\nimport { RegisterForm } from '../register-form';\n\ntest('should register new user and call onSuccess cb which should navigate the user to the app', async () => {\n  const newUser = createUser({});\n\n  const onSuccess = vi.fn();\n\n  await renderApp(\n    <RegisterForm\n      onSuccess={onSuccess}\n      chooseTeam={false}\n      setChooseTeam={() => {}}\n      teams={[]}\n    />,\n    { user: null },\n  );\n\n  await userEvent.type(screen.getByLabelText(/first name/i), newUser.firstName);\n  await userEvent.type(screen.getByLabelText(/last name/i), newUser.lastName);\n  await userEvent.type(screen.getByLabelText(/email address/i), newUser.email);\n  await userEvent.type(screen.getByLabelText(/password/i), newUser.password);\n  await userEvent.type(screen.getByLabelText(/team name/i), newUser.teamName);\n\n  await userEvent.click(screen.getByRole('button', { name: /register/i }));\n\n  await waitFor(() => expect(onSuccess).toHaveBeenCalledTimes(1));\n});\n"
  },
  {
    "path": "apps/nextjs-app/src/features/auth/components/login-form.tsx",
    "content": "'use client';\n\nimport NextLink from 'next/link';\nimport { useSearchParams } from 'next/navigation';\n\nimport { Button } from '@/components/ui/button';\nimport { Form, Input } from '@/components/ui/form';\nimport { paths } from '@/config/paths';\nimport { useLogin, loginInputSchema } from '@/lib/auth';\n\ntype LoginFormProps = {\n  onSuccess: () => void;\n};\n\nexport const LoginForm = ({ onSuccess }: LoginFormProps) => {\n  const login = useLogin({\n    onSuccess,\n  });\n\n  const searchParams = useSearchParams();\n  const redirectTo = searchParams?.get('redirectTo');\n  return (\n    <div>\n      <Form\n        onSubmit={(values) => {\n          login.mutate(values);\n        }}\n        schema={loginInputSchema}\n      >\n        {({ register, formState }) => (\n          <>\n            <Input\n              type=\"email\"\n              label=\"Email Address\"\n              error={formState.errors['email']}\n              registration={register('email')}\n            />\n            <Input\n              type=\"password\"\n              label=\"Password\"\n              error={formState.errors['password']}\n              registration={register('password')}\n            />\n            <div>\n              <Button\n                isLoading={login.isPending}\n                type=\"submit\"\n                className=\"w-full\"\n              >\n                Log in\n              </Button>\n            </div>\n          </>\n        )}\n      </Form>\n      <div className=\"mt-2 flex items-center justify-end\">\n        <div className=\"text-sm\">\n          <NextLink\n            href={paths.auth.register.getHref(redirectTo)}\n            className=\"font-medium text-blue-600 hover:text-blue-500\"\n          >\n            Register\n          </NextLink>\n        </div>\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/features/auth/components/register-form.tsx",
    "content": "'use client';\n\nimport NextLink from 'next/link';\nimport { useSearchParams } from 'next/navigation';\nimport * as React from 'react';\n\nimport { Button } from '@/components/ui/button';\nimport { Form, Input, Select, Label, Switch } from '@/components/ui/form';\nimport { paths } from '@/config/paths';\nimport { useRegister, registerInputSchema } from '@/lib/auth';\nimport { Team } from '@/types/api';\n\ntype RegisterFormProps = {\n  onSuccess: () => void;\n  chooseTeam: boolean;\n  setChooseTeam: () => void;\n  teams?: Team[];\n};\n\nexport const RegisterForm = ({\n  onSuccess,\n  chooseTeam,\n  setChooseTeam,\n  teams,\n}: RegisterFormProps) => {\n  const registering = useRegister({ onSuccess });\n  const searchParams = useSearchParams();\n  const redirectTo = searchParams?.get('redirectTo');\n\n  return (\n    <div>\n      <Form\n        onSubmit={(values) => {\n          registering.mutate(values);\n        }}\n        schema={registerInputSchema}\n        options={{\n          shouldUnregister: true,\n        }}\n      >\n        {({ register, formState }) => (\n          <>\n            <Input\n              type=\"text\"\n              label=\"First Name\"\n              error={formState.errors['firstName']}\n              registration={register('firstName')}\n            />\n            <Input\n              type=\"text\"\n              label=\"Last Name\"\n              error={formState.errors['lastName']}\n              registration={register('lastName')}\n            />\n            <Input\n              type=\"email\"\n              label=\"Email Address\"\n              error={formState.errors['email']}\n              registration={register('email')}\n            />\n            <Input\n              type=\"password\"\n              label=\"Password\"\n              error={formState.errors['password']}\n              registration={register('password')}\n            />\n\n            <div className=\"flex items-center space-x-2\">\n              <Switch\n                checked={chooseTeam}\n                onCheckedChange={setChooseTeam}\n                className={`${\n                  chooseTeam ? 'bg-blue-600' : 'bg-gray-200'\n                } 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`}\n                id=\"choose-team\"\n              />\n              <Label htmlFor=\"airplane-mode\">Join Existing Team</Label>\n            </div>\n\n            {chooseTeam && teams ? (\n              <Select\n                label=\"Team\"\n                error={formState.errors['teamId']}\n                registration={register('teamId')}\n                options={teams?.map((team) => ({\n                  label: team.name,\n                  value: team.id,\n                }))}\n              />\n            ) : (\n              <Input\n                type=\"text\"\n                label=\"Team Name\"\n                error={formState.errors['teamName']}\n                registration={register('teamName')}\n              />\n            )}\n            <div>\n              <Button\n                isLoading={registering.isPending}\n                type=\"submit\"\n                className=\"w-full\"\n              >\n                Register\n              </Button>\n            </div>\n          </>\n        )}\n      </Form>\n      <div className=\"mt-2 flex items-center justify-end\">\n        <div className=\"text-sm\">\n          <NextLink\n            href={paths.auth.login.getHref(redirectTo)}\n            className=\"font-medium text-blue-600 hover:text-blue-500\"\n          >\n            Log In\n          </NextLink>\n        </div>\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/features/comments/api/create-comment.ts",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query';\nimport { z } from 'zod';\n\nimport { api } from '@/lib/api-client';\nimport { MutationConfig } from '@/lib/react-query';\nimport { Comment } from '@/types/api';\n\nimport { getInfiniteCommentsQueryOptions } from './get-comments';\n\nexport const createCommentInputSchema = z.object({\n  discussionId: z.string().min(1, 'Required'),\n  body: z.string().min(1, 'Required'),\n});\n\nexport type CreateCommentInput = z.infer<typeof createCommentInputSchema>;\n\nexport const createComment = ({\n  data,\n}: {\n  data: CreateCommentInput;\n}): Promise<Comment> => {\n  return api.post('/comments', data);\n};\n\ntype UseCreateCommentOptions = {\n  discussionId: string;\n  mutationConfig?: MutationConfig<typeof createComment>;\n};\n\nexport const useCreateComment = ({\n  mutationConfig,\n  discussionId,\n}: UseCreateCommentOptions) => {\n  const queryClient = useQueryClient();\n\n  const { onSuccess, ...restConfig } = mutationConfig || {};\n\n  return useMutation({\n    onSuccess: (...args) => {\n      queryClient.invalidateQueries({\n        queryKey: getInfiniteCommentsQueryOptions(discussionId).queryKey,\n      });\n      onSuccess?.(...args);\n    },\n    ...restConfig,\n    mutationFn: createComment,\n  });\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/features/comments/api/delete-comment.ts",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query';\n\nimport { api } from '@/lib/api-client';\nimport { MutationConfig } from '@/lib/react-query';\n\nimport { getInfiniteCommentsQueryOptions } from './get-comments';\n\nexport const deleteComment = ({ commentId }: { commentId: string }) => {\n  return api.delete(`/comments/${commentId}`);\n};\n\ntype UseDeleteCommentOptions = {\n  discussionId: string;\n  mutationConfig?: MutationConfig<typeof deleteComment>;\n};\n\nexport const useDeleteComment = ({\n  mutationConfig,\n  discussionId,\n}: UseDeleteCommentOptions) => {\n  const queryClient = useQueryClient();\n\n  const { onSuccess, ...restConfig } = mutationConfig || {};\n\n  return useMutation({\n    onSuccess: (...args) => {\n      queryClient.invalidateQueries({\n        queryKey: getInfiniteCommentsQueryOptions(discussionId).queryKey,\n      });\n      onSuccess?.(...args);\n    },\n    ...restConfig,\n    mutationFn: deleteComment,\n  });\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/features/comments/api/get-comments.ts",
    "content": "import { infiniteQueryOptions, useInfiniteQuery } from '@tanstack/react-query';\n\nimport { api } from '@/lib/api-client';\nimport { QueryConfig } from '@/lib/react-query';\nimport { Comment, Meta } from '@/types/api';\n\nexport const getComments = ({\n  discussionId,\n  page = 1,\n}: {\n  discussionId: string;\n  page?: number;\n}): Promise<{ data: Comment[]; meta: Meta }> => {\n  return api.get(`/comments`, {\n    params: {\n      discussionId,\n      page,\n    },\n  });\n};\n\nexport const getInfiniteCommentsQueryOptions = (discussionId: string) => {\n  return infiniteQueryOptions({\n    queryKey: ['comments', discussionId],\n    queryFn: ({ pageParam = 1 }) => {\n      return getComments({ discussionId, page: pageParam as number });\n    },\n    getNextPageParam: (lastPage) => {\n      if (lastPage?.meta?.page === lastPage?.meta?.totalPages) return undefined;\n      const nextPage = lastPage.meta.page + 1;\n      return nextPage;\n    },\n    initialPageParam: 1,\n  });\n};\n\ntype UseCommentsOptions = {\n  discussionId: string;\n  page?: number;\n  queryConfig?: QueryConfig<typeof getComments>;\n};\n\nexport const useInfiniteComments = ({ discussionId }: UseCommentsOptions) => {\n  return useInfiniteQuery({\n    ...getInfiniteCommentsQueryOptions(discussionId),\n  });\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/features/comments/components/comments-list.tsx",
    "content": "'use client';\n\nimport { ArchiveX } from 'lucide-react';\nimport { usePathname } from 'next/navigation';\n\nimport { Button } from '@/components/ui/button';\nimport { MDPreview } from '@/components/ui/md-preview';\nimport { Spinner } from '@/components/ui/spinner';\nimport { useUser } from '@/lib/auth';\nimport { canDeleteComment } from '@/lib/authorization';\nimport { formatDate } from '@/utils/format';\n\nimport { useInfiniteComments } from '../api/get-comments';\n\nimport { DeleteComment } from './delete-comment';\n\ntype CommentsListProps = {\n  discussionId: string;\n};\n\nexport const CommentsList = ({ discussionId }: CommentsListProps) => {\n  const user = useUser();\n  const commentsQuery = useInfiniteComments({ discussionId });\n  const pathname = usePathname();\n  const isPublicView = pathname?.startsWith?.('/public/');\n\n  if (commentsQuery.isLoading) {\n    return (\n      <div className=\"flex h-48 w-full items-center justify-center\">\n        <Spinner size=\"lg\" />\n      </div>\n    );\n  }\n\n  const comments = commentsQuery.data?.pages.flatMap((page) => page.data);\n\n  if (!comments?.length)\n    return (\n      <div\n        role=\"list\"\n        aria-label=\"comments\"\n        className=\"flex h-40 flex-col items-center justify-center bg-white text-gray-500\"\n      >\n        <ArchiveX className=\"size-10\" />\n        <h4>No Comments Found</h4>\n      </div>\n    );\n\n  return (\n    <>\n      <ul aria-label=\"comments\" className=\"flex flex-col space-y-3\">\n        {comments.map((comment, index) => (\n          <li\n            aria-label={`comment-${comment.body}-${index}`}\n            key={comment.id || index}\n            className=\"w-full bg-white p-4 shadow-sm\"\n          >\n            <div className=\"flex justify-between\">\n              <div>\n                <span className=\"text-xs font-semibold\">\n                  {formatDate(comment.createdAt)}\n                </span>\n                {comment.author && (\n                  <span className=\"text-xs font-bold\">\n                    {' '}\n                    by {comment.author.firstName} {comment.author.lastName}\n                  </span>\n                )}\n              </div>\n              {!isPublicView && canDeleteComment(user.data, comment) && (\n                <DeleteComment discussionId={discussionId} id={comment.id} />\n              )}\n            </div>\n            <MDPreview value={comment.body} />\n          </li>\n        ))}\n      </ul>\n      {commentsQuery.hasNextPage && (\n        <div className=\"flex items-center justify-center py-4\">\n          <Button onClick={() => commentsQuery.fetchNextPage()}>\n            {commentsQuery.isFetchingNextPage ? (\n              <Spinner />\n            ) : (\n              'Load More Comments'\n            )}\n          </Button>\n        </div>\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/features/comments/components/comments.tsx",
    "content": "'use client';\n\nimport { usePathname } from 'next/navigation';\n\nimport { CommentsList } from './comments-list';\nimport { CreateComment } from './create-comment';\n\ntype CommentsProps = {\n  discussionId: string;\n};\n\nexport const Comments = ({ discussionId }: CommentsProps) => {\n  const pathname = usePathname();\n  const isPublicView = pathname?.startsWith?.('/public/');\n  return (\n    <div>\n      <div className=\"mb-4 flex items-center justify-between\">\n        <h3 className=\"text-xl font-bold\">Comments:</h3>\n        {!isPublicView && <CreateComment discussionId={discussionId} />}\n      </div>\n      <CommentsList discussionId={discussionId} />\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/features/comments/components/create-comment.tsx",
    "content": "'use client';\n\nimport { Plus } from 'lucide-react';\n\nimport { Button } from '@/components/ui/button';\nimport { Form, FormDrawer, Textarea } from '@/components/ui/form';\nimport { useNotifications } from '@/components/ui/notifications';\n\nimport {\n  useCreateComment,\n  createCommentInputSchema,\n} from '../api/create-comment';\n\ntype CreateCommentProps = {\n  discussionId: string;\n};\n\nexport const CreateComment = ({ discussionId }: CreateCommentProps) => {\n  const { addNotification } = useNotifications();\n  const createCommentMutation = useCreateComment({\n    discussionId,\n    mutationConfig: {\n      onSuccess: () => {\n        addNotification({\n          type: 'success',\n          title: 'Comment Created',\n        });\n      },\n    },\n  });\n\n  return (\n    <FormDrawer\n      isDone={createCommentMutation.isSuccess}\n      triggerButton={\n        <Button size=\"sm\" icon={<Plus className=\"size-4\" />}>\n          Create Comment\n        </Button>\n      }\n      title=\"Create Comment\"\n      submitButton={\n        <Button\n          isLoading={createCommentMutation.isPending}\n          form=\"create-comment\"\n          type=\"submit\"\n          size=\"sm\"\n          disabled={createCommentMutation.isPending}\n        >\n          Submit\n        </Button>\n      }\n    >\n      <Form\n        id=\"create-comment\"\n        onSubmit={(values) => {\n          createCommentMutation.mutate({\n            data: values,\n          });\n        }}\n        schema={createCommentInputSchema}\n        options={{\n          defaultValues: {\n            body: '',\n            discussionId: discussionId,\n          },\n        }}\n      >\n        {({ register, formState }) => (\n          <Textarea\n            label=\"Body\"\n            error={formState.errors['body']}\n            registration={register('body')}\n          />\n        )}\n      </Form>\n    </FormDrawer>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/features/comments/components/delete-comment.tsx",
    "content": "'use client';\n\nimport { Trash } from 'lucide-react';\n\nimport { Button } from '@/components/ui/button';\nimport { ConfirmationDialog } from '@/components/ui/dialog';\nimport { useNotifications } from '@/components/ui/notifications';\n\nimport { useDeleteComment } from '../api/delete-comment';\n\ntype DeleteCommentProps = {\n  id: string;\n  discussionId: string;\n};\n\nexport const DeleteComment = ({ id, discussionId }: DeleteCommentProps) => {\n  const { addNotification } = useNotifications();\n  const deleteCommentMutation = useDeleteComment({\n    discussionId,\n    mutationConfig: {\n      onSuccess: () => {\n        addNotification({\n          type: 'success',\n          title: 'Comment Deleted',\n        });\n      },\n    },\n  });\n\n  return (\n    <ConfirmationDialog\n      isDone={deleteCommentMutation.isSuccess}\n      icon=\"danger\"\n      title=\"Delete Comment\"\n      body=\"Are you sure you want to delete this comment?\"\n      triggerButton={\n        <Button\n          variant=\"destructive\"\n          size=\"sm\"\n          icon={<Trash className=\"size-4\" />}\n        >\n          Delete Comment\n        </Button>\n      }\n      confirmButton={\n        <Button\n          isLoading={deleteCommentMutation.isPending}\n          type=\"button\"\n          variant=\"destructive\"\n          onClick={() => deleteCommentMutation.mutate({ commentId: id })}\n        >\n          Delete Comment\n        </Button>\n      }\n    />\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/features/discussions/api/create-discussion.ts",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query';\nimport { z } from 'zod';\n\nimport { api } from '@/lib/api-client';\nimport { MutationConfig } from '@/lib/react-query';\nimport { Discussion } from '@/types/api';\n\nimport { getDiscussionsQueryOptions } from './get-discussions';\n\nexport const createDiscussionInputSchema = z.object({\n  title: z.string().min(1, 'Required'),\n  body: z.string().min(1, 'Required'),\n  public: z.boolean(),\n});\n\nexport type CreateDiscussionInput = z.infer<typeof createDiscussionInputSchema>;\n\nexport const createDiscussion = ({\n  data,\n}: {\n  data: CreateDiscussionInput;\n}): Promise<Discussion> => {\n  return api.post(`/discussions`, data);\n};\n\ntype UseCreateDiscussionOptions = {\n  mutationConfig?: MutationConfig<typeof createDiscussion>;\n};\n\nexport const useCreateDiscussion = ({\n  mutationConfig,\n}: UseCreateDiscussionOptions = {}) => {\n  const queryClient = useQueryClient();\n\n  const { onSuccess, ...restConfig } = mutationConfig || {};\n\n  return useMutation({\n    onSuccess: (...args) => {\n      queryClient.invalidateQueries({\n        queryKey: getDiscussionsQueryOptions().queryKey,\n      });\n      onSuccess?.(...args);\n    },\n    ...restConfig,\n    mutationFn: createDiscussion,\n  });\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/features/discussions/api/delete-discussion.ts",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query';\n\nimport { api } from '@/lib/api-client';\nimport { MutationConfig } from '@/lib/react-query';\n\nimport { getDiscussionsQueryOptions } from './get-discussions';\n\nexport const deleteDiscussion = ({\n  discussionId,\n}: {\n  discussionId: string;\n}) => {\n  return api.delete(`/discussions/${discussionId}`);\n};\n\ntype UseDeleteDiscussionOptions = {\n  mutationConfig?: MutationConfig<typeof deleteDiscussion>;\n};\n\nexport const useDeleteDiscussion = ({\n  mutationConfig,\n}: UseDeleteDiscussionOptions = {}) => {\n  const queryClient = useQueryClient();\n\n  const { onSuccess, ...restConfig } = mutationConfig || {};\n\n  return useMutation({\n    onSuccess: (...args) => {\n      queryClient.invalidateQueries({\n        queryKey: getDiscussionsQueryOptions().queryKey,\n      });\n      onSuccess?.(...args);\n    },\n    ...restConfig,\n    mutationFn: deleteDiscussion,\n  });\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/features/discussions/api/get-discussion.ts",
    "content": "import { useQuery, queryOptions } from '@tanstack/react-query';\n\nimport { api } from '@/lib/api-client';\nimport { QueryConfig } from '@/lib/react-query';\nimport { Discussion } from '@/types/api';\n\nexport const getDiscussion = ({\n  discussionId,\n}: {\n  discussionId: string;\n}): Promise<{ data: Discussion }> => {\n  return api.get(`/discussions/${discussionId}`);\n};\n\nexport const getDiscussionQueryOptions = (discussionId: string) => {\n  return queryOptions({\n    queryKey: ['discussions', discussionId],\n    queryFn: () => getDiscussion({ discussionId }),\n  });\n};\n\ntype UseDiscussionOptions = {\n  discussionId: string;\n  queryConfig?: QueryConfig<typeof getDiscussionQueryOptions>;\n};\n\nexport const useDiscussion = ({\n  discussionId,\n  queryConfig,\n}: UseDiscussionOptions) => {\n  return useQuery({\n    ...getDiscussionQueryOptions(discussionId),\n    ...queryConfig,\n  });\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/features/discussions/api/get-discussions.ts",
    "content": "import { queryOptions, useQuery } from '@tanstack/react-query';\n\nimport { api } from '@/lib/api-client';\nimport { QueryConfig } from '@/lib/react-query';\nimport { Discussion, Meta } from '@/types/api';\n\nexport const getDiscussions = (\n  { page }: { page?: number } = { page: 1 },\n): Promise<{\n  data: Discussion[];\n  meta: Meta;\n}> => {\n  return api.get(`/discussions`, {\n    params: {\n      page,\n    },\n  });\n};\n\nexport const getDiscussionsQueryOptions = ({\n  page = 1,\n}: { page?: number } = {}) => {\n  return queryOptions({\n    queryKey: ['discussions', { page }],\n    queryFn: () => getDiscussions({ page }),\n  });\n};\n\ntype UseDiscussionsOptions = {\n  page?: number;\n  queryConfig?: QueryConfig<typeof getDiscussionsQueryOptions>;\n};\n\nexport const useDiscussions = ({\n  queryConfig,\n  page,\n}: UseDiscussionsOptions) => {\n  return useQuery({\n    ...getDiscussionsQueryOptions({ page }),\n    ...queryConfig,\n  });\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/features/discussions/api/update-discussion.ts",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query';\nimport { z } from 'zod';\n\nimport { api } from '@/lib/api-client';\nimport { MutationConfig } from '@/lib/react-query';\nimport { Discussion } from '@/types/api';\n\nimport { getDiscussionQueryOptions } from './get-discussion';\n\nexport const updateDiscussionInputSchema = z.object({\n  title: z.string().min(1, 'Required'),\n  body: z.string().min(1, 'Required'),\n  public: z.boolean(),\n});\n\nexport type UpdateDiscussionInput = z.infer<typeof updateDiscussionInputSchema>;\n\nexport const updateDiscussion = ({\n  data,\n  discussionId,\n}: {\n  data: UpdateDiscussionInput;\n  discussionId: string;\n}): Promise<Discussion> => {\n  return api.patch(`/discussions/${discussionId}`, data);\n};\n\ntype UseUpdateDiscussionOptions = {\n  mutationConfig?: MutationConfig<typeof updateDiscussion>;\n};\n\nexport const useUpdateDiscussion = ({\n  mutationConfig,\n}: UseUpdateDiscussionOptions = {}) => {\n  const queryClient = useQueryClient();\n\n  const { onSuccess, ...restConfig } = mutationConfig || {};\n\n  return useMutation({\n    onSuccess: (data, ...args) => {\n      queryClient.refetchQueries({\n        queryKey: getDiscussionQueryOptions(data.id).queryKey,\n      });\n      onSuccess?.(data, ...args);\n    },\n    ...restConfig,\n    mutationFn: updateDiscussion,\n  });\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/features/discussions/components/create-discussion.tsx",
    "content": "'use client';\n\nimport { Plus } from 'lucide-react';\n\nimport { Button } from '@/components/ui/button';\nimport {\n  Form,\n  FormDrawer,\n  Input,\n  Label,\n  Switch,\n  Textarea,\n} from '@/components/ui/form';\nimport { useNotifications } from '@/components/ui/notifications';\nimport { useUser } from '@/lib/auth';\nimport { canCreateDiscussion } from '@/lib/authorization';\n\nimport {\n  createDiscussionInputSchema,\n  useCreateDiscussion,\n} from '../api/create-discussion';\n\nexport const CreateDiscussion = () => {\n  const { addNotification } = useNotifications();\n  const createDiscussionMutation = useCreateDiscussion({\n    mutationConfig: {\n      onSuccess: () => {\n        addNotification({\n          type: 'success',\n          title: 'Discussion Created',\n        });\n      },\n    },\n  });\n\n  const user = useUser();\n\n  if (!canCreateDiscussion(user?.data)) {\n    return null;\n  }\n\n  return (\n    <FormDrawer\n      isDone={createDiscussionMutation.isSuccess}\n      triggerButton={\n        <Button size=\"sm\" icon={<Plus className=\"size-4\" />}>\n          Create Discussion\n        </Button>\n      }\n      title=\"Create Discussion\"\n      submitButton={\n        <Button\n          form=\"create-discussion\"\n          type=\"submit\"\n          size=\"sm\"\n          isLoading={createDiscussionMutation.isPending}\n        >\n          Submit\n        </Button>\n      }\n    >\n      <Form\n        id=\"create-discussion\"\n        onSubmit={(values) => {\n          createDiscussionMutation.mutate({ data: values });\n        }}\n        schema={createDiscussionInputSchema}\n        options={{\n          defaultValues: {\n            title: '',\n            body: '',\n            public: false,\n          },\n        }}\n      >\n        {({ register, formState, setValue, watch }) => (\n          <>\n            <Input\n              label=\"Title\"\n              error={formState.errors['title']}\n              registration={register('title')}\n            />\n\n            <Textarea\n              label=\"Body\"\n              error={formState.errors['body']}\n              registration={register('body')}\n            />\n\n            <div className=\"flex items-center space-x-2\">\n              <Switch\n                name=\"public\"\n                onCheckedChange={(value) => setValue('public', value)}\n                checked={watch('public')}\n                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`}\n                id=\"public\"\n              />\n              <Label htmlFor=\"airplane-mode\">Public</Label>\n            </div>\n          </>\n        )}\n      </Form>\n    </FormDrawer>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/features/discussions/components/delete-discussion.tsx",
    "content": "'use client';\n\nimport { Trash } from 'lucide-react';\n\nimport { Button } from '@/components/ui/button';\nimport { ConfirmationDialog } from '@/components/ui/dialog';\nimport { useNotifications } from '@/components/ui/notifications';\nimport { useUser } from '@/lib/auth';\nimport { canDeleteDiscussion } from '@/lib/authorization';\n\nimport { useDeleteDiscussion } from '../api/delete-discussion';\n\ntype DeleteDiscussionProps = {\n  id: string;\n};\n\nexport const DeleteDiscussion = ({ id }: DeleteDiscussionProps) => {\n  const user = useUser();\n  const { addNotification } = useNotifications();\n  const deleteDiscussionMutation = useDeleteDiscussion({\n    mutationConfig: {\n      onSuccess: () => {\n        addNotification({\n          type: 'success',\n          title: 'Discussion Deleted',\n        });\n      },\n    },\n  });\n\n  if (!canDeleteDiscussion(user?.data)) {\n    return null;\n  }\n\n  return (\n    <ConfirmationDialog\n      icon=\"danger\"\n      title=\"Delete Discussion\"\n      body=\"Are you sure you want to delete this discussion?\"\n      triggerButton={\n        <Button variant=\"destructive\" icon={<Trash className=\"size-4\" />}>\n          Delete Discussion\n        </Button>\n      }\n      confirmButton={\n        <Button\n          isLoading={deleteDiscussionMutation.isPending}\n          type=\"button\"\n          variant=\"destructive\"\n          onClick={() => deleteDiscussionMutation.mutate({ discussionId: id })}\n        >\n          Delete Discussion\n        </Button>\n      }\n    />\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/features/discussions/components/discussion-view.tsx",
    "content": "'use client';\n\nimport { Link as LinkIcon } from 'lucide-react';\nimport { usePathname } from 'next/navigation';\n\nimport { Link } from '@/components/ui/link';\nimport { MDPreview } from '@/components/ui/md-preview';\nimport { Spinner } from '@/components/ui/spinner';\nimport { paths } from '@/config/paths';\nimport { formatDate } from '@/utils/format';\n\nimport { useDiscussion } from '../api/get-discussion';\nimport { UpdateDiscussion } from '../components/update-discussion';\n\nexport const DiscussionView = ({ discussionId }: { discussionId: string }) => {\n  const pathname = usePathname();\n  const isPublicView = pathname?.startsWith?.('/public/');\n\n  const discussionQuery = useDiscussion({\n    discussionId,\n  });\n\n  if (discussionQuery.isLoading) {\n    return (\n      <div className=\"flex h-48 w-full items-center justify-center\">\n        <Spinner size=\"lg\" />\n      </div>\n    );\n  }\n\n  const discussion = discussionQuery?.data?.data;\n\n  if (!discussion) return null;\n\n  return (\n    <div>\n      <div className=\"flex justify-between\">\n        <span>\n          <span className=\"text-xs font-bold\">\n            {formatDate(discussion.createdAt)}\n          </span>\n          {discussion.author && (\n            <span className=\"ml-2 text-sm font-bold\">\n              by {discussion.author.firstName} {discussion.author.lastName}\n            </span>\n          )}\n        </span>\n        {!isPublicView && discussion.public && (\n          <Link\n            className=\"ml-2 flex items-center gap-2 text-sm font-bold\"\n            href={paths.public.discussion.getHref(discussionId)}\n            target=\"_blank\"\n          >\n            View Public Version <LinkIcon size={16} />\n          </Link>\n        )}\n      </div>\n      <div className=\"mt-6 flex flex-col space-y-16\">\n        {!isPublicView && (\n          <div className=\"flex justify-end\">\n            <UpdateDiscussion discussionId={discussionId} />\n          </div>\n        )}\n        <div>\n          <div className=\"overflow-hidden bg-white shadow sm:rounded-lg\">\n            <div className=\"px-4 py-5 sm:px-6\">\n              <div className=\"mt-1 max-w-2xl text-sm text-gray-500\">\n                <MDPreview value={discussion.body} />\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/features/discussions/components/discussions-list.tsx",
    "content": "'use client';\n\nimport { useQueryClient } from '@tanstack/react-query';\nimport { useSearchParams } from 'next/navigation';\n\nimport { Link } from '@/components/ui/link';\nimport { Spinner } from '@/components/ui/spinner';\nimport { Table } from '@/components/ui/table';\nimport { paths } from '@/config/paths';\nimport { formatDate } from '@/utils/format';\n\nimport { getDiscussionQueryOptions } from '../api/get-discussion';\nimport { useDiscussions } from '../api/get-discussions';\n\nimport { DeleteDiscussion } from './delete-discussion';\n\nexport type DiscussionsListProps = {\n  onDiscussionPrefetch?: (id: string) => void;\n};\n\nexport const DiscussionsList = ({\n  onDiscussionPrefetch,\n}: DiscussionsListProps) => {\n  const searchParams = useSearchParams();\n  const page = searchParams?.get('page') ? Number(searchParams.get('page')) : 1;\n\n  const discussionsQuery = useDiscussions({\n    page: page,\n  });\n  const queryClient = useQueryClient();\n\n  if (discussionsQuery.isLoading) {\n    return (\n      <div className=\"flex h-48 w-full items-center justify-center\">\n        <Spinner size=\"lg\" />\n      </div>\n    );\n  }\n\n  const discussions = discussionsQuery.data?.data;\n  const meta = discussionsQuery.data?.meta;\n\n  if (!discussions) return null;\n\n  return (\n    <Table\n      data={discussions}\n      columns={[\n        {\n          title: 'Title',\n          field: 'title',\n        },\n        {\n          title: 'Created At',\n          field: 'createdAt',\n          Cell({ entry: { createdAt } }) {\n            return <span>{formatDate(createdAt)}</span>;\n          },\n        },\n        {\n          title: '',\n          field: 'id',\n          Cell({ entry: { id } }) {\n            return (\n              <Link\n                onMouseEnter={() => {\n                  // Prefetch the discussion data when the user hovers over the link\n                  queryClient.prefetchQuery(getDiscussionQueryOptions(id));\n                  onDiscussionPrefetch?.(id);\n                }}\n                href={paths.app.discussion.getHref(id)}\n              >\n                View\n              </Link>\n            );\n          },\n        },\n        {\n          title: '',\n          field: 'id',\n          Cell({ entry: { id } }) {\n            return <DeleteDiscussion id={id} />;\n          },\n        },\n      ]}\n      pagination={\n        meta && {\n          totalPages: meta.totalPages,\n          currentPage: meta.page,\n          rootUrl: '',\n        }\n      }\n    />\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/features/discussions/components/update-discussion.tsx",
    "content": "'use client';\n\nimport { Pen } from 'lucide-react';\n\nimport { Button } from '@/components/ui/button';\nimport {\n  Form,\n  FormDrawer,\n  Input,\n  Label,\n  Switch,\n  Textarea,\n} from '@/components/ui/form';\nimport { useNotifications } from '@/components/ui/notifications';\nimport { useUser } from '@/lib/auth';\nimport { canUpdateDiscussion } from '@/lib/authorization';\n\nimport { useDiscussion } from '../api/get-discussion';\nimport {\n  updateDiscussionInputSchema,\n  useUpdateDiscussion,\n} from '../api/update-discussion';\n\ntype UpdateDiscussionProps = {\n  discussionId: string;\n};\n\nexport const UpdateDiscussion = ({ discussionId }: UpdateDiscussionProps) => {\n  const { addNotification } = useNotifications();\n  const discussionQuery = useDiscussion({ discussionId });\n  const updateDiscussionMutation = useUpdateDiscussion({\n    mutationConfig: {\n      onSuccess: () => {\n        addNotification({\n          type: 'success',\n          title: 'Discussion Updated',\n        });\n      },\n    },\n  });\n\n  const user = useUser();\n\n  if (!canUpdateDiscussion(user?.data)) {\n    return null;\n  }\n\n  const discussion = discussionQuery.data?.data;\n\n  return (\n    <FormDrawer\n      isDone={updateDiscussionMutation.isSuccess}\n      triggerButton={\n        <Button icon={<Pen className=\"size-4\" />} size=\"sm\">\n          Update Discussion\n        </Button>\n      }\n      title=\"Update Discussion\"\n      submitButton={\n        <Button\n          form=\"update-discussion\"\n          type=\"submit\"\n          size=\"sm\"\n          isLoading={updateDiscussionMutation.isPending}\n        >\n          Submit\n        </Button>\n      }\n    >\n      <Form\n        id=\"update-discussion\"\n        onSubmit={(values) => {\n          updateDiscussionMutation.mutate({\n            data: values,\n            discussionId,\n          });\n        }}\n        options={{\n          defaultValues: {\n            title: discussion?.title ?? '',\n            body: discussion?.body ?? '',\n            public: discussion?.public ?? false,\n          },\n        }}\n        schema={updateDiscussionInputSchema}\n      >\n        {({ register, formState, setValue, watch }) => (\n          <>\n            <Input\n              label=\"Title\"\n              error={formState.errors['title']}\n              registration={register('title')}\n            />\n            <Textarea\n              label=\"Body\"\n              error={formState.errors['body']}\n              registration={register('body')}\n            />\n\n            <div className=\"flex items-center space-x-2\">\n              <Switch\n                name=\"public\"\n                onCheckedChange={(value) => setValue('public', value)}\n                checked={watch('public')}\n                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`}\n                id=\"public\"\n              />\n              <Label htmlFor=\"airplane-mode\">Public</Label>\n            </div>\n          </>\n        )}\n      </Form>\n    </FormDrawer>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/features/teams/api/get-teams.ts",
    "content": "import { queryOptions, useQuery } from '@tanstack/react-query';\n\nimport { api } from '@/lib/api-client';\nimport { QueryConfig } from '@/lib/react-query';\nimport { Team } from '@/types/api';\n\nexport const getTeams = (): Promise<{ data: Team[] }> => {\n  return api.get('/teams');\n};\n\nexport const getTeamsQueryOptions = () => {\n  return queryOptions({\n    queryKey: ['teams'],\n    queryFn: () => getTeams(),\n  });\n};\n\ntype UseTeamsOptions = {\n  queryConfig?: QueryConfig<typeof getTeamsQueryOptions>;\n};\n\nexport const useTeams = ({ queryConfig = {} }: UseTeamsOptions = {}) => {\n  return useQuery({\n    ...getTeamsQueryOptions(),\n    ...queryConfig,\n  });\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/features/users/api/delete-user.ts",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query';\n\nimport { api } from '@/lib/api-client';\nimport { MutationConfig } from '@/lib/react-query';\n\nimport { getUsersQueryOptions } from './get-users';\n\nexport type DeleteUserDTO = {\n  userId: string;\n};\n\nexport const deleteUser = ({ userId }: DeleteUserDTO) => {\n  return api.delete(`/users/${userId}`);\n};\n\ntype UseDeleteUserOptions = {\n  mutationConfig?: MutationConfig<typeof deleteUser>;\n};\n\nexport const useDeleteUser = ({\n  mutationConfig,\n}: UseDeleteUserOptions = {}) => {\n  const queryClient = useQueryClient();\n\n  const { onSuccess, ...restConfig } = mutationConfig || {};\n\n  return useMutation({\n    onSuccess: (...args) => {\n      queryClient.invalidateQueries({\n        queryKey: getUsersQueryOptions().queryKey,\n      });\n      onSuccess?.(...args);\n    },\n    ...restConfig,\n    mutationFn: deleteUser,\n  });\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/features/users/api/get-users.ts",
    "content": "import { queryOptions, useQuery } from '@tanstack/react-query';\n\nimport { api } from '@/lib/api-client';\nimport { QueryConfig } from '@/lib/react-query';\nimport { User } from '@/types/api';\n\nexport const getUsers = (): Promise<{ data: User[] }> => {\n  return api.get(`/users`);\n};\n\nexport const getUsersQueryOptions = () => {\n  return queryOptions({\n    queryKey: ['users'],\n    queryFn: getUsers,\n  });\n};\n\ntype UseUsersOptions = {\n  queryConfig?: QueryConfig<typeof getUsersQueryOptions>;\n};\n\nexport const useUsers = ({ queryConfig }: UseUsersOptions = {}) => {\n  return useQuery({\n    ...getUsersQueryOptions(),\n    ...queryConfig,\n  });\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/features/users/api/update-profile.ts",
    "content": "import { useMutation } from '@tanstack/react-query';\nimport { z } from 'zod';\n\nimport { api } from '@/lib/api-client';\nimport { useUser } from '@/lib/auth';\nimport { MutationConfig } from '@/lib/react-query';\n\nexport const updateProfileInputSchema = z.object({\n  email: z.string().min(1, 'Required').email('Invalid email'),\n  firstName: z.string().min(1, 'Required'),\n  lastName: z.string().min(1, 'Required'),\n  bio: z.string(),\n});\n\nexport type UpdateProfileInput = z.infer<typeof updateProfileInputSchema>;\n\nexport const updateProfile = ({ data }: { data: UpdateProfileInput }) => {\n  return api.patch(`/users/profile`, data);\n};\n\ntype UseUpdateProfileOptions = {\n  mutationConfig?: MutationConfig<typeof updateProfile>;\n};\n\nexport const useUpdateProfile = ({\n  mutationConfig,\n}: UseUpdateProfileOptions = {}) => {\n  const { refetch: refetchUser } = useUser();\n\n  const { onSuccess, ...restConfig } = mutationConfig || {};\n\n  return useMutation({\n    onSuccess: (...args) => {\n      refetchUser();\n      onSuccess?.(...args);\n    },\n    ...restConfig,\n    mutationFn: updateProfile,\n  });\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/features/users/components/delete-user.tsx",
    "content": "'use client';\n\nimport { Button } from '@/components/ui/button';\nimport { ConfirmationDialog } from '@/components/ui/dialog';\nimport { useNotifications } from '@/components/ui/notifications';\nimport { useUser } from '@/lib/auth';\n\nimport { useDeleteUser } from '../api/delete-user';\n\ntype DeleteUserProps = {\n  id: string;\n};\n\nexport const DeleteUser = ({ id }: DeleteUserProps) => {\n  const user = useUser();\n  const { addNotification } = useNotifications();\n  const deleteUserMutation = useDeleteUser({\n    mutationConfig: {\n      onSuccess: () => {\n        addNotification({\n          type: 'success',\n          title: 'User Deleted',\n        });\n      },\n    },\n  });\n\n  if (user.data?.id === id) return null;\n\n  return (\n    <ConfirmationDialog\n      icon=\"danger\"\n      title=\"Delete User\"\n      body=\"Are you sure you want to delete this user?\"\n      triggerButton={<Button variant=\"destructive\">Delete</Button>}\n      confirmButton={\n        <Button\n          isLoading={deleteUserMutation.isPending}\n          type=\"button\"\n          variant=\"destructive\"\n          onClick={() => deleteUserMutation.mutate({ userId: id })}\n        >\n          Delete User\n        </Button>\n      }\n    />\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/features/users/components/update-profile.tsx",
    "content": "'use client';\n\nimport { Pen } from 'lucide-react';\n\nimport { Button } from '@/components/ui/button';\nimport { Form, FormDrawer, Input, Textarea } from '@/components/ui/form';\nimport { useNotifications } from '@/components/ui/notifications';\nimport { useUser } from '@/lib/auth';\n\nimport {\n  updateProfileInputSchema,\n  useUpdateProfile,\n} from '../api/update-profile';\n\nexport const UpdateProfile = () => {\n  const user = useUser();\n  const { addNotification } = useNotifications();\n  const updateProfileMutation = useUpdateProfile({\n    mutationConfig: {\n      onSuccess: () => {\n        addNotification({\n          type: 'success',\n          title: 'Profile Updated',\n        });\n      },\n    },\n  });\n\n  return (\n    <FormDrawer\n      isDone={updateProfileMutation.isSuccess}\n      triggerButton={\n        <Button icon={<Pen className=\"size-4\" />} size=\"sm\">\n          Update Profile\n        </Button>\n      }\n      title=\"Update Profile\"\n      submitButton={\n        <Button\n          form=\"update-profile\"\n          type=\"submit\"\n          size=\"sm\"\n          isLoading={updateProfileMutation.isPending}\n        >\n          Submit\n        </Button>\n      }\n    >\n      <Form\n        id=\"update-profile\"\n        onSubmit={(values) => {\n          updateProfileMutation.mutate({ data: values });\n        }}\n        options={{\n          defaultValues: {\n            firstName: user.data?.firstName ?? '',\n            lastName: user.data?.lastName ?? '',\n            email: user.data?.email ?? '',\n            bio: user.data?.bio ?? '',\n          },\n        }}\n        schema={updateProfileInputSchema}\n      >\n        {({ register, formState }) => (\n          <>\n            <Input\n              label=\"First Name\"\n              error={formState.errors['firstName']}\n              registration={register('firstName')}\n            />\n            <Input\n              label=\"Last Name\"\n              error={formState.errors['lastName']}\n              registration={register('lastName')}\n            />\n            <Input\n              label=\"Email Address\"\n              type=\"email\"\n              error={formState.errors['email']}\n              registration={register('email')}\n            />\n\n            <Textarea\n              label=\"Bio\"\n              error={formState.errors['bio']}\n              registration={register('bio')}\n            />\n          </>\n        )}\n      </Form>\n    </FormDrawer>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/features/users/components/users-list.tsx",
    "content": "'use client';\n\nimport { Spinner } from '@/components/ui/spinner';\nimport { Table } from '@/components/ui/table';\nimport { formatDate } from '@/utils/format';\n\nimport { useUsers } from '../api/get-users';\n\nimport { DeleteUser } from './delete-user';\n\nexport const UsersList = () => {\n  const usersQuery = useUsers();\n\n  if (usersQuery.isLoading) {\n    return (\n      <div className=\"flex h-48 w-full items-center justify-center\">\n        <Spinner size=\"lg\" />\n      </div>\n    );\n  }\n\n  const users = usersQuery.data?.data;\n\n  if (!users) return null;\n\n  return (\n    <Table\n      data={users}\n      columns={[\n        {\n          title: 'First Name',\n          field: 'firstName',\n        },\n        {\n          title: 'Last Name',\n          field: 'lastName',\n        },\n        {\n          title: 'Email',\n          field: 'email',\n        },\n        {\n          title: 'Role',\n          field: 'role',\n        },\n        {\n          title: 'Created At',\n          field: 'createdAt',\n          Cell({ entry: { createdAt } }) {\n            return <span>{formatDate(createdAt)}</span>;\n          },\n        },\n        {\n          title: '',\n          field: 'id',\n          Cell({ entry: { id } }) {\n            return <DeleteUser id={id} />;\n          },\n        },\n      ]}\n    />\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/hooks/__tests__/use-disclosure.test.ts",
    "content": "import { renderHook, act } from '@testing-library/react';\n\nimport { useDisclosure } from '../use-disclosure';\n\ntest('should open the state', () => {\n  const { result } = renderHook(() => useDisclosure());\n\n  expect(result.current.isOpen).toBe(false);\n\n  act(() => {\n    result.current.open();\n  });\n\n  expect(result.current.isOpen).toBe(true);\n});\n\ntest('should close the state', () => {\n  const { result } = renderHook(() => useDisclosure());\n\n  expect(result.current.isOpen).toBe(false);\n\n  act(() => {\n    result.current.close();\n  });\n\n  expect(result.current.isOpen).toBe(false);\n});\n\ntest('should toggle the state', () => {\n  const { result } = renderHook(() => useDisclosure());\n\n  expect(result.current.isOpen).toBe(false);\n\n  act(() => {\n    result.current.toggle();\n  });\n\n  expect(result.current.isOpen).toBe(true);\n\n  act(() => {\n    result.current.toggle();\n  });\n\n  expect(result.current.isOpen).toBe(false);\n});\n\ntest('should define initial state', () => {\n  const { result } = renderHook(() => useDisclosure(true));\n\n  expect(result.current.isOpen).toBe(true);\n\n  act(() => {\n    result.current.toggle();\n  });\n\n  expect(result.current.isOpen).toBe(false);\n});\n"
  },
  {
    "path": "apps/nextjs-app/src/hooks/use-disclosure.ts",
    "content": "import * as React from 'react';\n\nexport const useDisclosure = (initial = false) => {\n  const [isOpen, setIsOpen] = React.useState(initial);\n\n  const open = React.useCallback(() => setIsOpen(true), []);\n  const close = React.useCallback(() => setIsOpen(false), []);\n  const toggle = React.useCallback(() => setIsOpen((state) => !state), []);\n\n  return { isOpen, open, close, toggle };\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/lib/__tests__/authorization.test.tsx",
    "content": "import { Comment, User } from '@/types/api';\n\nimport {\n  canCreateDiscussion,\n  canDeleteDiscussion,\n  canUpdateDiscussion,\n  canViewUsers,\n  canDeleteComment,\n} from '../authorization';\n\ndescribe('Discussion Authorization', () => {\n  const adminUser: User = {\n    id: '1',\n    role: 'ADMIN',\n  } as User;\n\n  const regularUser: User = {\n    id: '2',\n    role: 'USER',\n  } as User;\n\n  test('should allow admin to create discussions', () => {\n    expect(canCreateDiscussion(adminUser)).toBe(true);\n    expect(canCreateDiscussion(regularUser)).toBe(false);\n    expect(canCreateDiscussion(null)).toBe(false);\n    expect(canCreateDiscussion(undefined)).toBe(false);\n  });\n\n  test('should allow admin to delete discussions', () => {\n    expect(canDeleteDiscussion(adminUser)).toBe(true);\n    expect(canDeleteDiscussion(regularUser)).toBe(false);\n    expect(canDeleteDiscussion(null)).toBe(false);\n    expect(canDeleteDiscussion(undefined)).toBe(false);\n  });\n\n  test('should allow admin to update discussions', () => {\n    expect(canUpdateDiscussion(adminUser)).toBe(true);\n    expect(canUpdateDiscussion(regularUser)).toBe(false);\n    expect(canUpdateDiscussion(null)).toBe(false);\n    expect(canUpdateDiscussion(undefined)).toBe(false);\n  });\n\n  test('should allow admin to view users', () => {\n    expect(canViewUsers(adminUser)).toBe(true);\n    expect(canViewUsers(regularUser)).toBe(false);\n    expect(canViewUsers(null)).toBe(false);\n    expect(canViewUsers(undefined)).toBe(false);\n  });\n});\n\ndescribe('Comment Authorization', () => {\n  const adminUser: User = {\n    id: '1',\n    role: 'ADMIN',\n  } as User;\n\n  const regularUser: User = {\n    id: '2',\n    role: 'USER',\n  } as User;\n\n  const anotherUser: User = {\n    id: '3',\n    role: 'USER',\n  } as User;\n\n  test('should allow admin to delete any comment', () => {\n    const comment: Comment = {\n      id: '1',\n      author: anotherUser,\n    } as Comment;\n\n    expect(canDeleteComment(adminUser, comment)).toBe(true);\n  });\n\n  test('should allow users to delete their own comments', () => {\n    const comment: Comment = {\n      id: '1',\n      author: regularUser,\n    } as Comment;\n\n    expect(canDeleteComment(regularUser, comment)).toBe(true);\n  });\n\n  test('should not allow users to delete others comments', () => {\n    const comment: Comment = {\n      id: '1',\n      author: anotherUser,\n    } as Comment;\n\n    expect(canDeleteComment(regularUser, comment)).toBe(false);\n  });\n\n  test('should not allow unauthorized users to delete comments', () => {\n    const comment: Comment = {\n      id: '1',\n      author: regularUser,\n    } as Comment;\n\n    expect(canDeleteComment(null, comment)).toBe(false);\n    expect(canDeleteComment(undefined, comment)).toBe(false);\n  });\n});\n"
  },
  {
    "path": "apps/nextjs-app/src/lib/api-client.ts",
    "content": "import { useNotifications } from '@/components/ui/notifications';\nimport { env } from '@/config/env';\n\ntype RequestOptions = {\n  method?: string;\n  headers?: Record<string, string>;\n  body?: any;\n  cookie?: string;\n  params?: Record<string, string | number | boolean | undefined | null>;\n  cache?: RequestCache;\n  next?: NextFetchRequestConfig;\n};\n\nfunction buildUrlWithParams(\n  url: string,\n  params?: RequestOptions['params'],\n): string {\n  if (!params) return url;\n  const filteredParams = Object.fromEntries(\n    Object.entries(params).filter(\n      ([, value]) => value !== undefined && value !== null,\n    ),\n  );\n  if (Object.keys(filteredParams).length === 0) return url;\n  const queryString = new URLSearchParams(\n    filteredParams as Record<string, string>,\n  ).toString();\n  return `${url}?${queryString}`;\n}\n\n// Create a separate function for getting server-side cookies that can be imported where needed\nexport function getServerCookies() {\n  if (typeof window !== 'undefined') return '';\n\n  // Dynamic import next/headers only on server-side\n  return import('next/headers').then(({ cookies }) => {\n    try {\n      const cookieStore = cookies();\n      return cookieStore\n        .getAll()\n        .map((c) => `${c.name}=${c.value}`)\n        .join('; ');\n    } catch (error) {\n      console.error('Failed to access cookies:', error);\n      return '';\n    }\n  });\n}\n\nasync function fetchApi<T>(\n  url: string,\n  options: RequestOptions = {},\n): Promise<T> {\n  const {\n    method = 'GET',\n    headers = {},\n    body,\n    cookie,\n    params,\n    cache = 'no-store',\n    next,\n  } = options;\n\n  // Get cookies from the request when running on server\n  let cookieHeader = cookie;\n  if (typeof window === 'undefined' && !cookie) {\n    cookieHeader = await getServerCookies();\n  }\n\n  const fullUrl = buildUrlWithParams(`${env.API_URL}${url}`, params);\n\n  const response = await fetch(fullUrl, {\n    method,\n    headers: {\n      'Content-Type': 'application/json',\n      Accept: 'application/json',\n      ...headers,\n      ...(cookieHeader ? { Cookie: cookieHeader } : {}),\n    },\n    body: body ? JSON.stringify(body) : undefined,\n    credentials: 'include',\n    cache,\n    next,\n  });\n\n  if (!response.ok) {\n    const message = (await response.json()).message || response.statusText;\n    if (typeof window !== 'undefined') {\n      useNotifications.getState().addNotification({\n        type: 'error',\n        title: 'Error',\n        message,\n      });\n    }\n    throw new Error(message);\n  }\n\n  return response.json();\n}\n\nexport const api = {\n  get<T>(url: string, options?: RequestOptions): Promise<T> {\n    return fetchApi<T>(url, { ...options, method: 'GET' });\n  },\n  post<T>(url: string, body?: any, options?: RequestOptions): Promise<T> {\n    return fetchApi<T>(url, { ...options, method: 'POST', body });\n  },\n  put<T>(url: string, body?: any, options?: RequestOptions): Promise<T> {\n    return fetchApi<T>(url, { ...options, method: 'PUT', body });\n  },\n  patch<T>(url: string, body?: any, options?: RequestOptions): Promise<T> {\n    return fetchApi<T>(url, { ...options, method: 'PATCH', body });\n  },\n  delete<T>(url: string, options?: RequestOptions): Promise<T> {\n    return fetchApi<T>(url, { ...options, method: 'DELETE' });\n  },\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/lib/auth.tsx",
    "content": "import {\n  queryOptions,\n  useMutation,\n  useQuery,\n  useQueryClient,\n} from '@tanstack/react-query';\nimport { z } from 'zod';\n\nimport { AuthResponse, User } from '@/types/api';\n\nimport { api } from './api-client';\n\n// api call definitions for auth (types, schemas, requests):\n// these are not part of features as this is a module shared across features\n\nexport const getUser = async (): Promise<User> => {\n  const response = (await api.get('/auth/me')) as { data: User };\n\n  return response.data;\n};\n\nconst userQueryKey = ['user'];\n\nexport const getUserQueryOptions = () => {\n  return queryOptions({\n    queryKey: userQueryKey,\n    queryFn: getUser,\n  });\n};\n\nexport const useUser = () => useQuery(getUserQueryOptions());\n\nexport const useLogin = ({ onSuccess }: { onSuccess?: () => void }) => {\n  const queryClient = useQueryClient();\n  return useMutation({\n    mutationFn: loginWithEmailAndPassword,\n    onSuccess: (data) => {\n      queryClient.setQueryData(userQueryKey, data.user);\n      onSuccess?.();\n    },\n  });\n};\n\nexport const useRegister = ({ onSuccess }: { onSuccess?: () => void }) => {\n  const queryClient = useQueryClient();\n  return useMutation({\n    mutationFn: registerWithEmailAndPassword,\n    onSuccess: (data) => {\n      queryClient.setQueryData(userQueryKey, data.user);\n      onSuccess?.();\n    },\n  });\n};\n\nexport const useLogout = ({ onSuccess }: { onSuccess?: () => void }) => {\n  const queryClient = useQueryClient();\n  return useMutation({\n    mutationFn: logout,\n    onSuccess: () => {\n      queryClient.removeQueries({ queryKey: userQueryKey });\n      onSuccess?.();\n    },\n  });\n};\n\nconst logout = (): Promise<void> => {\n  return api.post('/auth/logout');\n};\n\nexport const loginInputSchema = z.object({\n  email: z.string().min(1, 'Required').email('Invalid email'),\n  password: z.string().min(5, 'Required'),\n});\n\nexport type LoginInput = z.infer<typeof loginInputSchema>;\nconst loginWithEmailAndPassword = (data: LoginInput): Promise<AuthResponse> => {\n  return api.post('/auth/login', data);\n};\n\nexport const registerInputSchema = z\n  .object({\n    email: z.string().min(1, 'Required'),\n    firstName: z.string().min(1, 'Required'),\n    lastName: z.string().min(1, 'Required'),\n    password: z.string().min(5, 'Required'),\n  })\n  .and(\n    z\n      .object({\n        teamId: z.string().min(1, 'Required'),\n        teamName: z.null().default(null),\n      })\n      .or(\n        z.object({\n          teamName: z.string().min(1, 'Required'),\n          teamId: z.null().default(null),\n        }),\n      ),\n  );\n\nexport type RegisterInput = z.infer<typeof registerInputSchema>;\n\nconst registerWithEmailAndPassword = (\n  data: RegisterInput,\n): Promise<AuthResponse> => {\n  return api.post('/auth/register', data);\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/lib/authorization.ts",
    "content": "import { Comment, User } from '@/types/api';\n\nexport const canCreateDiscussion = (user: User | null | undefined) => {\n  return user?.role === 'ADMIN';\n};\nexport const canDeleteDiscussion = (user: User | null | undefined) => {\n  return user?.role === 'ADMIN';\n};\nexport const canUpdateDiscussion = (user: User | null | undefined) => {\n  return user?.role === 'ADMIN';\n};\n\nexport const canViewUsers = (user: User | null | undefined) => {\n  return user?.role === 'ADMIN';\n};\n\nexport const canDeleteComment = (\n  user: User | null | undefined,\n  comment: Comment,\n) => {\n  if (user?.role === 'ADMIN') {\n    return true;\n  }\n\n  if (user?.role === 'USER' && comment.author?.id === user.id) {\n    return true;\n  }\n\n  return false;\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/lib/react-query.ts",
    "content": "import { UseMutationOptions, DefaultOptions } from '@tanstack/react-query';\n\nexport const queryConfig = {\n  queries: {\n    // throwOnError: true,\n    refetchOnWindowFocus: false,\n    retry: false,\n    staleTime: 1000 * 60,\n  },\n} satisfies DefaultOptions;\n\nexport type ApiFnReturnType<FnType extends (...args: any) => Promise<any>> =\n  Awaited<ReturnType<FnType>>;\n\nexport type QueryConfig<T extends (...args: any[]) => any> = Omit<\n  ReturnType<T>,\n  'queryKey' | 'queryFn'\n>;\n\nexport type MutationConfig<\n  MutationFnType extends (...args: any) => Promise<any>,\n> = UseMutationOptions<\n  ApiFnReturnType<MutationFnType>,\n  Error,\n  Parameters<MutationFnType>[0]\n>;\n"
  },
  {
    "path": "apps/nextjs-app/src/styles/globals.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer base {\n  :root {\n    --background: 0 0% 100%;\n    --foreground: 222.2 84% 4.9%;\n\n    --card: 0 0% 100%;\n    --card-foreground: 222.2 84% 4.9%;\n\n    --popover: 0 0% 100%;\n    --popover-foreground: 222.2 84% 4.9%;\n\n    --primary: 222.2 47.4% 11.2%;\n    --primary-foreground: 210 40% 98%;\n\n    --secondary: 210 40% 96.1%;\n    --secondary-foreground: 222.2 47.4% 11.2%;\n\n    --muted: 210 40% 96.1%;\n    --muted-foreground: 215.4 16.3% 46.9%;\n\n    --accent: 210 40% 96.1%;\n    --accent-foreground: 222.2 47.4% 11.2%;\n\n    --destructive: 0 84.2% 60.2%;\n    --destructive-foreground: 210 40% 98%;\n\n    --border: 214.3 31.8% 91.4%;\n    --input: 214.3 31.8% 91.4%;\n    --ring: 222.2 84% 4.9%;\n\n    --radius: 0.5rem;\n  }\n\n  .dark {\n    --background: 222.2 84% 4.9%;\n    --foreground: 210 40% 98%;\n\n    --card: 222.2 84% 4.9%;\n    --card-foreground: 210 40% 98%;\n\n    --popover: 222.2 84% 4.9%;\n    --popover-foreground: 210 40% 98%;\n\n    --primary: 210 40% 98%;\n    --primary-foreground: 222.2 47.4% 11.2%;\n\n    --secondary: 217.2 32.6% 17.5%;\n    --secondary-foreground: 210 40% 98%;\n\n    --muted: 217.2 32.6% 17.5%;\n    --muted-foreground: 215 20.2% 65.1%;\n\n    --accent: 217.2 32.6% 17.5%;\n    --accent-foreground: 210 40% 98%;\n\n    --destructive: 0 62.8% 30.6%;\n    --destructive-foreground: 210 40% 98%;\n\n    --border: 217.2 32.6% 17.5%;\n    --input: 217.2 32.6% 17.5%;\n    --ring: 212.7 26.8% 83.9%;\n  }\n}\n\n@layer base {\n  * {\n    @apply border-border;\n  }\n  body {\n    @apply bg-background text-foreground;\n  }\n}\n\nbody {\n  margin: 0;\n  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',\n    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',\n    sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',\n    monospace;\n}\n"
  },
  {
    "path": "apps/nextjs-app/src/testing/data-generators.ts",
    "content": "import {\n  randCompanyName,\n  randUserName,\n  randEmail,\n  randParagraph,\n  randUuid,\n  randPassword,\n  randCatchPhrase,\n} from '@ngneat/falso';\n\nconst generateUser = () => ({\n  id: randUuid() + Math.random(),\n  firstName: randUserName({ withAccents: false }),\n  lastName: randUserName({ withAccents: false }),\n  email: randEmail(),\n  password: randPassword(),\n  teamId: randUuid(),\n  teamName: randCompanyName(),\n  role: 'ADMIN',\n  bio: randParagraph(),\n  createdAt: Date.now(),\n});\n\nexport const createUser = <T extends Partial<ReturnType<typeof generateUser>>>(\n  overrides?: T,\n) => {\n  return { ...generateUser(), ...overrides };\n};\n\nconst generateTeam = () => ({\n  id: randUuid(),\n  name: randCompanyName(),\n  description: randParagraph(),\n  createdAt: Date.now(),\n});\n\nexport const createTeam = <T extends Partial<ReturnType<typeof generateTeam>>>(\n  overrides?: T,\n) => {\n  return { ...generateTeam(), ...overrides };\n};\n\nconst generateDiscussion = () => ({\n  id: randUuid(),\n  title: randCatchPhrase(),\n  body: randParagraph(),\n  createdAt: Date.now(),\n  public: true,\n});\n\nexport const createDiscussion = <\n  T extends Partial<ReturnType<typeof generateDiscussion>>,\n>(\n  overrides?: T & {\n    authorId?: string;\n    teamId?: string;\n  },\n) => {\n  return { ...generateDiscussion(), ...overrides };\n};\n\nconst generateComment = () => ({\n  id: randUuid(),\n  body: randParagraph(),\n  createdAt: Date.now(),\n});\n\nexport const createComment = <\n  T extends Partial<ReturnType<typeof generateComment>>,\n>(\n  overrides?: T & {\n    authorId?: string;\n    discussionId?: string;\n  },\n) => {\n  return { ...generateComment(), ...overrides };\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/testing/mocks/browser.ts",
    "content": "import { setupWorker } from 'msw/browser';\n\nimport { handlers } from './handlers';\n\nexport const worker = setupWorker(...handlers);\n"
  },
  {
    "path": "apps/nextjs-app/src/testing/mocks/db.ts",
    "content": "import { factory, primaryKey } from '@mswjs/data';\nimport { nanoid } from 'nanoid';\n\nconst models = {\n  user: {\n    id: primaryKey(nanoid),\n    firstName: String,\n    lastName: String,\n    email: String,\n    password: String,\n    teamId: String,\n    role: String,\n    bio: String,\n    createdAt: Date.now,\n  },\n  team: {\n    id: primaryKey(nanoid),\n    name: String,\n    description: String,\n    createdAt: Date.now,\n  },\n  discussion: {\n    id: primaryKey(nanoid),\n    title: String,\n    body: String,\n    authorId: String,\n    teamId: String,\n    createdAt: Date.now,\n    public: Boolean,\n  },\n  comment: {\n    id: primaryKey(nanoid),\n    body: String,\n    authorId: String,\n    discussionId: String,\n    createdAt: Date.now,\n  },\n};\n\nexport const db = factory(models);\n\nexport type Model = keyof typeof models;\n\nconst dbFilePath = 'mocked-db.json';\n\nexport const loadDb = async () => {\n  // If we are running in a Node.js environment\n  if (typeof window === 'undefined') {\n    const { readFile, writeFile } = await import('fs/promises');\n    try {\n      const data = await readFile(dbFilePath, 'utf8');\n      return JSON.parse(data);\n    } catch (error: any) {\n      if (error?.code === 'ENOENT') {\n        const emptyDB = {};\n        await writeFile(dbFilePath, JSON.stringify(emptyDB, null, 2));\n        return emptyDB;\n      } else {\n        console.error('Error loading mocked DB:', error);\n        return null;\n      }\n    }\n  }\n  // If we are running in a browser environment\n  return Object.assign(\n    JSON.parse(window.localStorage.getItem('msw-db') || '{}'),\n  );\n};\n\nexport const storeDb = async (data: string) => {\n  // If we are running in a Node.js environment\n  if (typeof window === 'undefined') {\n    const { writeFile } = await import('fs/promises');\n    await writeFile(dbFilePath, data);\n  } else {\n    // If we are running in a browser environment\n    window.localStorage.setItem('msw-db', data);\n  }\n};\n\nexport const persistDb = async (model: Model) => {\n  if (process.env.NODE_ENV === 'test') return;\n  const data = await loadDb();\n  data[model] = db[model].getAll();\n  await storeDb(JSON.stringify(data));\n};\n\nexport const initializeDb = async () => {\n  const database = await loadDb();\n  Object.entries(db).forEach(([key, model]) => {\n    const dataEntres = database[key];\n    if (dataEntres) {\n      dataEntres?.forEach((entry: Record<string, any>) => {\n        model.create(entry);\n      });\n    }\n  });\n};\n\nexport const resetDb = () => {\n  window.localStorage.clear();\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/testing/mocks/handlers/auth.ts",
    "content": "import Cookies from 'js-cookie';\nimport { HttpResponse, http } from 'msw';\n\nimport { env } from '@/config/env';\n\nimport { db, persistDb } from '../db';\nimport {\n  authenticate,\n  hash,\n  requireAuth,\n  AUTH_COOKIE,\n  networkDelay,\n} from '../utils';\n\ntype RegisterBody = {\n  firstName: string;\n  lastName: string;\n  email: string;\n  password: string;\n  teamId?: string;\n  teamName?: string;\n};\n\ntype LoginBody = {\n  email: string;\n  password: string;\n};\n\nexport const authHandlers = [\n  http.post(`${env.API_URL}/auth/register`, async ({ request }) => {\n    await networkDelay();\n    try {\n      const userObject = (await request.json()) as RegisterBody;\n\n      const existingUser = db.user.findFirst({\n        where: {\n          email: {\n            equals: userObject.email,\n          },\n        },\n      });\n\n      if (existingUser) {\n        return HttpResponse.json(\n          { message: 'The user already exists' },\n          { status: 400 },\n        );\n      }\n\n      let teamId;\n      let role;\n\n      if (!userObject.teamId) {\n        const team = db.team.create({\n          name: userObject.teamName ?? `${userObject.firstName} Team`,\n        });\n        await persistDb('team');\n        teamId = team.id;\n        role = 'ADMIN';\n      } else {\n        const existingTeam = db.team.findFirst({\n          where: {\n            id: {\n              equals: userObject.teamId,\n            },\n          },\n        });\n\n        if (!existingTeam) {\n          return HttpResponse.json(\n            {\n              message: 'The team you are trying to join does not exist!',\n            },\n            { status: 400 },\n          );\n        }\n        teamId = userObject.teamId;\n        role = 'USER';\n      }\n\n      db.user.create({\n        ...userObject,\n        role,\n        password: hash(userObject.password),\n        teamId,\n      });\n\n      await persistDb('user');\n\n      const result = authenticate({\n        email: userObject.email,\n        password: userObject.password,\n      });\n\n      // todo: remove once tests in Github Actions are fixed\n      Cookies.set(AUTH_COOKIE, result.jwt, { path: '/' });\n\n      return HttpResponse.json(result, {\n        headers: {\n          // with a real API servier, the token cookie should also be Secure and HttpOnly\n          'Set-Cookie': `${AUTH_COOKIE}=${result.jwt}; Path=/;`,\n        },\n      });\n    } catch (error: any) {\n      return HttpResponse.json(\n        { message: error?.message || 'Server Error' },\n        { status: 500 },\n      );\n    }\n  }),\n\n  http.post(`${env.API_URL}/auth/login`, async ({ request }) => {\n    await networkDelay();\n\n    try {\n      const credentials = (await request.json()) as LoginBody;\n      const result = authenticate(credentials);\n\n      // todo: remove once tests in Github Actions are fixed\n      Cookies.set(AUTH_COOKIE, result.jwt, { path: '/' });\n\n      return HttpResponse.json(result, {\n        headers: {\n          // with a real API servier, the token cookie should also be Secure and HttpOnly\n          'Set-Cookie': `${AUTH_COOKIE}=${result.jwt}; Path=/;`,\n        },\n      });\n    } catch (error: any) {\n      return HttpResponse.json(\n        { message: error?.message || 'Server Error' },\n        { status: 500 },\n      );\n    }\n  }),\n\n  http.post(`${env.API_URL}/auth/logout`, async () => {\n    await networkDelay();\n\n    // todo: remove once tests in Github Actions are fixed\n    Cookies.remove(AUTH_COOKIE);\n\n    return HttpResponse.json(\n      { message: 'Logged out' },\n      {\n        headers: {\n          'Set-Cookie': `${AUTH_COOKIE}=; Path=/;`,\n        },\n      },\n    );\n  }),\n\n  http.get(`${env.API_URL}/auth/me`, async ({ cookies }) => {\n    await networkDelay();\n\n    try {\n      const { user } = requireAuth(cookies);\n      return HttpResponse.json({ data: user });\n    } catch (error: any) {\n      return HttpResponse.json(\n        { message: error?.message || 'Server Error' },\n        { status: 500 },\n      );\n    }\n  }),\n];\n"
  },
  {
    "path": "apps/nextjs-app/src/testing/mocks/handlers/comments.ts",
    "content": "import { HttpResponse, http } from 'msw';\n\nimport { env } from '@/config/env';\n\nimport { db, persistDb } from '../db';\nimport { networkDelay, requireAuth, sanitizeUser } from '../utils';\n\ntype CreateCommentBody = {\n  body: string;\n  discussionId: string;\n};\n\nexport const commentsHandlers = [\n  http.get(`${env.API_URL}/comments`, async ({ request, cookies }) => {\n    await networkDelay();\n\n    try {\n      const url = new URL(request.url);\n      const discussionId = url.searchParams.get('discussionId') || '';\n      const page = Number(url.searchParams.get('page') || 1);\n\n      const discussion = db.discussion.findFirst({\n        where: {\n          id: {\n            equals: discussionId,\n          },\n        },\n      });\n\n      if (!discussion?.public) {\n        const { error } = requireAuth(cookies);\n        if (error) {\n          return HttpResponse.json({ message: error }, { status: 401 });\n        }\n      }\n\n      const total = db.comment.count({\n        where: {\n          discussionId: {\n            equals: discussionId,\n          },\n        },\n      });\n\n      const totalPages = Math.ceil(total / 10);\n\n      const comments = db.comment\n        .findMany({\n          where: {\n            discussionId: {\n              equals: discussionId,\n            },\n          },\n          take: 10,\n          skip: 10 * (page - 1),\n        })\n        .map(({ authorId, ...comment }) => {\n          const author = db.user.findFirst({\n            where: {\n              id: {\n                equals: authorId,\n              },\n            },\n          });\n          return {\n            ...comment,\n            author: author ? sanitizeUser(author) : {},\n          };\n        });\n      return HttpResponse.json({\n        data: comments,\n        meta: {\n          page,\n          total,\n          totalPages,\n        },\n      });\n    } catch (error: any) {\n      return HttpResponse.json(\n        { message: error?.message || 'Server Error' },\n        { status: 500 },\n      );\n    }\n  }),\n\n  http.post(`${env.API_URL}/comments`, async ({ request, cookies }) => {\n    await networkDelay();\n\n    try {\n      const { user, error } = requireAuth(cookies);\n      if (error) {\n        return HttpResponse.json({ message: error }, { status: 401 });\n      }\n      const data = (await request.json()) as CreateCommentBody;\n      const result = db.comment.create({\n        authorId: user?.id,\n        ...data,\n      });\n      await persistDb('comment');\n      return HttpResponse.json(result);\n    } catch (error: any) {\n      return HttpResponse.json(\n        { message: error?.message || 'Server Error' },\n        { status: 500 },\n      );\n    }\n  }),\n\n  http.delete(\n    `${env.API_URL}/comments/:commentId`,\n    async ({ params, cookies }) => {\n      await networkDelay();\n\n      try {\n        const { user, error } = requireAuth(cookies);\n        if (error) {\n          return HttpResponse.json({ message: error }, { status: 401 });\n        }\n        const commentId = params.commentId as string;\n        const result = db.comment.delete({\n          where: {\n            id: {\n              equals: commentId,\n            },\n            ...(user?.role === 'USER' && {\n              authorId: {\n                equals: user.id,\n              },\n            }),\n          },\n        });\n        await persistDb('comment');\n        return HttpResponse.json(result);\n      } catch (error: any) {\n        return HttpResponse.json(\n          { message: error?.message || 'Server Error' },\n          { status: 500 },\n        );\n      }\n    },\n  ),\n];\n"
  },
  {
    "path": "apps/nextjs-app/src/testing/mocks/handlers/discussions.ts",
    "content": "import { HttpResponse, http } from 'msw';\n\nimport { env } from '@/config/env';\n\nimport { db, persistDb } from '../db';\nimport {\n  requireAuth,\n  requireAdmin,\n  sanitizeUser,\n  networkDelay,\n} from '../utils';\n\ntype DiscussionBody = {\n  title: string;\n  body: string;\n  public: boolean;\n};\n\nexport const discussionsHandlers = [\n  http.get(`${env.API_URL}/discussions`, async ({ cookies, request }) => {\n    await networkDelay();\n\n    try {\n      const { user, error } = requireAuth(cookies);\n      if (error) {\n        return HttpResponse.json({ message: error }, { status: 401 });\n      }\n\n      const url = new URL(request.url);\n\n      const page = Number(url.searchParams.get('page') || 1);\n\n      const total = db.discussion.count({\n        where: {\n          teamId: {\n            equals: user?.teamId,\n          },\n        },\n      });\n\n      const totalPages = Math.ceil(total / 10);\n\n      const result = db.discussion\n        .findMany({\n          where: {\n            teamId: {\n              equals: user?.teamId,\n            },\n          },\n          take: 10,\n          skip: 10 * (page - 1),\n        })\n        .map(({ authorId, ...discussion }) => {\n          const author = db.user.findFirst({\n            where: {\n              id: {\n                equals: authorId,\n              },\n            },\n          });\n          return {\n            ...discussion,\n            author: author ? sanitizeUser(author) : {},\n          };\n        });\n      return HttpResponse.json({\n        data: result,\n        meta: {\n          page,\n          total,\n          totalPages,\n        },\n      });\n    } catch (error: any) {\n      return HttpResponse.json(\n        { message: error?.message || 'Server Error' },\n        { status: 500 },\n      );\n    }\n  }),\n\n  http.get(\n    `${env.API_URL}/discussions/:discussionId`,\n    async ({ params, cookies }) => {\n      await networkDelay();\n\n      const discussionId = params.discussionId as string;\n\n      const discussion = db.discussion.findFirst({\n        where: {\n          id: {\n            equals: discussionId,\n          },\n        },\n      });\n\n      if (discussion?.public) {\n        const author = db.user.findFirst({\n          where: {\n            id: {\n              equals: discussion.authorId,\n            },\n          },\n        });\n\n        const result = {\n          ...discussion,\n          author: author ? sanitizeUser(author) : {},\n        };\n\n        return HttpResponse.json({ data: result });\n      }\n\n      try {\n        const { user, error } = requireAuth(cookies);\n        if (error) {\n          return HttpResponse.json({ message: error }, { status: 401 });\n        }\n        const discussion = db.discussion.findFirst({\n          where: {\n            id: {\n              equals: discussionId,\n            },\n            teamId: {\n              equals: user?.teamId,\n            },\n          },\n        });\n\n        if (!discussion) {\n          return HttpResponse.json(\n            { message: 'Discussion not found' },\n            { status: 404 },\n          );\n        }\n\n        const author = db.user.findFirst({\n          where: {\n            id: {\n              equals: discussion.authorId,\n            },\n          },\n        });\n\n        const result = {\n          ...discussion,\n          author: author ? sanitizeUser(author) : {},\n        };\n\n        return HttpResponse.json({ data: result });\n      } catch (error: any) {\n        return HttpResponse.json(\n          { message: error?.message || 'Server Error' },\n          { status: 500 },\n        );\n      }\n    },\n  ),\n\n  http.post(`${env.API_URL}/discussions`, async ({ request, cookies }) => {\n    await networkDelay();\n\n    try {\n      const { user, error } = requireAuth(cookies);\n      if (error) {\n        return HttpResponse.json({ message: error }, { status: 401 });\n      }\n      const data = (await request.json()) as DiscussionBody;\n      requireAdmin(user);\n      const result = db.discussion.create({\n        teamId: user?.teamId,\n        authorId: user?.id,\n        ...data,\n      });\n      await persistDb('discussion');\n      return HttpResponse.json(result);\n    } catch (error: any) {\n      return HttpResponse.json(\n        { message: error?.message || 'Server Error' },\n        { status: 500 },\n      );\n    }\n  }),\n\n  http.patch(\n    `${env.API_URL}/discussions/:discussionId`,\n    async ({ request, params, cookies }) => {\n      await networkDelay();\n\n      try {\n        const { user, error } = requireAuth(cookies);\n        if (error) {\n          return HttpResponse.json({ message: error }, { status: 401 });\n        }\n        const data = (await request.json()) as DiscussionBody;\n        const discussionId = params.discussionId as string;\n        requireAdmin(user);\n        const result = db.discussion.update({\n          where: {\n            teamId: {\n              equals: user?.teamId,\n            },\n            id: {\n              equals: discussionId,\n            },\n          },\n          data,\n        });\n        await persistDb('discussion');\n        return HttpResponse.json(result);\n      } catch (error: any) {\n        return HttpResponse.json(\n          { message: error?.message || 'Server Error' },\n          { status: 500 },\n        );\n      }\n    },\n  ),\n\n  http.delete(\n    `${env.API_URL}/discussions/:discussionId`,\n    async ({ cookies, params }) => {\n      await networkDelay();\n\n      try {\n        const { user, error } = requireAuth(cookies);\n        if (error) {\n          return HttpResponse.json({ message: error }, { status: 401 });\n        }\n        const discussionId = params.discussionId as string;\n        requireAdmin(user);\n        const result = db.discussion.delete({\n          where: {\n            id: {\n              equals: discussionId,\n            },\n          },\n        });\n        await persistDb('discussion');\n        return HttpResponse.json(result);\n      } catch (error: any) {\n        return HttpResponse.json(\n          { message: error?.message || 'Server Error' },\n          { status: 500 },\n        );\n      }\n    },\n  ),\n];\n"
  },
  {
    "path": "apps/nextjs-app/src/testing/mocks/handlers/index.ts",
    "content": "import { HttpResponse, http } from 'msw';\n\nimport { env } from '@/config/env';\n\nimport { networkDelay } from '../utils';\n\nimport { authHandlers } from './auth';\nimport { commentsHandlers } from './comments';\nimport { discussionsHandlers } from './discussions';\nimport { teamsHandlers } from './teams';\nimport { usersHandlers } from './users';\n\nexport const handlers = [\n  ...authHandlers,\n  ...commentsHandlers,\n  ...discussionsHandlers,\n  ...teamsHandlers,\n  ...usersHandlers,\n  http.get(`${env.API_URL}/healthcheck`, async () => {\n    await networkDelay();\n    return HttpResponse.json({ ok: true });\n  }),\n];\n"
  },
  {
    "path": "apps/nextjs-app/src/testing/mocks/handlers/teams.ts",
    "content": "import { HttpResponse, http } from 'msw';\n\nimport { env } from '@/config/env';\n\nimport { db } from '../db';\nimport { networkDelay } from '../utils';\n\nexport const teamsHandlers = [\n  http.get(`${env.API_URL}/teams`, async () => {\n    await networkDelay();\n\n    try {\n      const result = db.team.getAll();\n      return HttpResponse.json({ data: result });\n    } catch (error: any) {\n      return HttpResponse.json(\n        { message: error?.message || 'Server Error' },\n        { status: 500 },\n      );\n    }\n  }),\n];\n"
  },
  {
    "path": "apps/nextjs-app/src/testing/mocks/handlers/users.ts",
    "content": "import { HttpResponse, http } from 'msw';\n\nimport { env } from '@/config/env';\n\nimport { db, persistDb } from '../db';\nimport {\n  requireAuth,\n  requireAdmin,\n  sanitizeUser,\n  networkDelay,\n} from '../utils';\n\ntype ProfileBody = {\n  email: string;\n  firstName: string;\n  lastName: string;\n  bio: string;\n};\n\nexport const usersHandlers = [\n  http.get(`${env.API_URL}/users`, async ({ cookies }) => {\n    await networkDelay();\n\n    try {\n      const { user, error } = requireAuth(cookies);\n      if (error) {\n        return HttpResponse.json({ message: error }, { status: 401 });\n      }\n      const result = db.user\n        .findMany({\n          where: {\n            teamId: {\n              equals: user?.teamId,\n            },\n          },\n        })\n        .map(sanitizeUser);\n\n      return HttpResponse.json({ data: result });\n    } catch (error: any) {\n      return HttpResponse.json(\n        { message: error?.message || 'Server Error' },\n        { status: 500 },\n      );\n    }\n  }),\n\n  http.patch(`${env.API_URL}/users/profile`, async ({ request, cookies }) => {\n    await networkDelay();\n\n    try {\n      const { user, error } = requireAuth(cookies);\n      if (error) {\n        return HttpResponse.json({ message: error }, { status: 401 });\n      }\n      const data = (await request.json()) as ProfileBody;\n      const result = db.user.update({\n        where: {\n          id: {\n            equals: user?.id,\n          },\n        },\n        data,\n      });\n      await persistDb('user');\n      return HttpResponse.json(result);\n    } catch (error: any) {\n      return HttpResponse.json(\n        { message: error?.message || 'Server Error' },\n        { status: 500 },\n      );\n    }\n  }),\n\n  http.delete(`${env.API_URL}/users/:userId`, async ({ cookies, params }) => {\n    await networkDelay();\n\n    try {\n      const { user, error } = requireAuth(cookies);\n      if (error) {\n        return HttpResponse.json({ message: error }, { status: 401 });\n      }\n      const userId = params.userId as string;\n      requireAdmin(user);\n      const result = db.user.delete({\n        where: {\n          id: {\n            equals: userId,\n          },\n          teamId: {\n            equals: user?.teamId,\n          },\n        },\n      });\n      await persistDb('user');\n      return HttpResponse.json(result);\n    } catch (error: any) {\n      return HttpResponse.json(\n        { message: error?.message || 'Server Error' },\n        { status: 500 },\n      );\n    }\n  }),\n];\n"
  },
  {
    "path": "apps/nextjs-app/src/testing/mocks/index.ts",
    "content": "import { env } from '@/config/env';\n\nexport const enableMocking = async () => {\n  if (env.ENABLE_API_MOCKING) {\n    const { worker } = await import('./browser');\n    const { initializeDb } = await import('./db');\n    await initializeDb();\n    return worker.start();\n  }\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/testing/mocks/server.ts",
    "content": "import { setupServer } from 'msw/node';\n\nimport { handlers } from './handlers';\n\nexport const server = setupServer(...handlers);\n"
  },
  {
    "path": "apps/nextjs-app/src/testing/mocks/utils.ts",
    "content": "import Cookies from 'js-cookie';\nimport { delay } from 'msw';\n\nimport { db } from './db';\n\nexport const encode = (obj: any) => {\n  const btoa =\n    typeof window === 'undefined'\n      ? (str: string) => Buffer.from(str, 'binary').toString('base64')\n      : window.btoa;\n  return btoa(JSON.stringify(obj));\n};\n\nexport const decode = (str: string) => {\n  const atob =\n    typeof window === 'undefined'\n      ? (str: string) => Buffer.from(str, 'base64').toString('binary')\n      : window.atob;\n  return JSON.parse(atob(str));\n};\n\nexport const hash = (str: string) => {\n  let hash = 5381,\n    i = str.length;\n\n  while (i) {\n    hash = (hash * 33) ^ str.charCodeAt(--i);\n  }\n  return String(hash >>> 0);\n};\n\nexport const networkDelay = () => {\n  const delayTime = process.env.TEST\n    ? 200\n    : Math.floor(Math.random() * 700) + 300;\n  return delay(delayTime);\n};\n\nconst omit = <T extends object>(obj: T, keys: string[]): T => {\n  const result = {} as T;\n  for (const key in obj) {\n    if (!keys.includes(key)) {\n      result[key] = obj[key];\n    }\n  }\n\n  return result;\n};\n\nexport const sanitizeUser = <O extends object>(user: O) =>\n  omit<O>(user, ['password', 'iat']);\n\nexport function authenticate({\n  email,\n  password,\n}: {\n  email: string;\n  password: string;\n}) {\n  const user = db.user.findFirst({\n    where: {\n      email: {\n        equals: email,\n      },\n    },\n  });\n\n  if (user?.password === hash(password)) {\n    const sanitizedUser = sanitizeUser(user);\n    const encodedToken = encode(sanitizedUser);\n    return { user: sanitizedUser, jwt: encodedToken };\n  }\n\n  const error = new Error('Invalid username or password');\n  throw error;\n}\n\nexport const AUTH_COOKIE = `bulletproof_react_app_token`;\n\nexport function requireAuth(cookies: Record<string, string>) {\n  try {\n    const encodedToken = cookies[AUTH_COOKIE] || Cookies.get(AUTH_COOKIE);\n    if (!encodedToken) {\n      return { error: 'Unauthorized', user: null };\n    }\n    const decodedToken = decode(encodedToken) as { id: string };\n\n    const user = db.user.findFirst({\n      where: {\n        id: {\n          equals: decodedToken.id,\n        },\n      },\n    });\n\n    if (!user) {\n      return { error: 'Unauthorized', user: null };\n    }\n\n    return { user: sanitizeUser(user) };\n  } catch (err: any) {\n    return { error: 'Unauthorized', user: null };\n  }\n}\n\nexport function requireAdmin(user: any) {\n  if (user.role !== 'ADMIN') {\n    throw Error('Unauthorized');\n  }\n}\n"
  },
  {
    "path": "apps/nextjs-app/src/testing/setup-tests.ts",
    "content": "import '@testing-library/jest-dom/vitest';\n\nimport { initializeDb, resetDb } from '@/testing/mocks/db';\nimport { server } from '@/testing/mocks/server';\n\nvi.mock('zustand');\n\nbeforeAll(() => {\n  server.listen({ onUnhandledRequest: 'error' });\n  vi.mock('next/navigation', async () => {\n    const actual = await vi.importActual('next/navigation');\n    return {\n      ...actual,\n      useRouter: () => {\n        return {\n          push: vi.fn(),\n          replace: vi.fn(),\n        };\n      },\n      usePathname: () => '/app',\n      useSearchParams: () => ({\n        get: vi.fn(),\n      }),\n    };\n  });\n});\nafterAll(() => server.close());\nbeforeEach(() => {\n  const ResizeObserverMock = vi.fn(() => ({\n    observe: vi.fn(),\n    unobserve: vi.fn(),\n    disconnect: vi.fn(),\n  }));\n\n  vi.stubGlobal('ResizeObserver', ResizeObserverMock);\n\n  window.btoa = (str: string) => Buffer.from(str, 'binary').toString('base64');\n  window.atob = (str: string) => Buffer.from(str, 'base64').toString('binary');\n\n  initializeDb();\n});\nafterEach(() => {\n  server.resetHandlers();\n  resetDb();\n});\n"
  },
  {
    "path": "apps/nextjs-app/src/testing/test-utils.tsx",
    "content": "import {\n  render as rtlRender,\n  waitForElementToBeRemoved,\n  screen,\n} from '@testing-library/react';\nimport userEvent from '@testing-library/user-event';\nimport Cookies from 'js-cookie';\n\nimport { AppProvider } from '@/app/provider';\n\nimport {\n  createDiscussion as generateDiscussion,\n  createUser as generateUser,\n} from './data-generators';\nimport { db } from './mocks/db';\nimport { AUTH_COOKIE, authenticate, hash } from './mocks/utils';\n\nexport const waitForLoadingToFinish = () =>\n  waitForElementToBeRemoved(\n    () => [\n      ...screen.queryAllByTestId(/loading/i),\n      ...screen.queryAllByText(/loading/i),\n    ],\n    { timeout: 4000 },\n  );\n\nexport const createUser = async (userProperties?: any) => {\n  const user = generateUser(userProperties) as any;\n  await db.user.create({ ...user, password: hash(user.password) });\n  return user;\n};\n\nexport const createDiscussion = async (discussionProperties?: any) => {\n  const discussion = generateDiscussion(discussionProperties);\n  const res = await db.discussion.create(discussion);\n  return res;\n};\n\nexport const loginAsUser = async (user: any) => {\n  const authUser = await authenticate(user);\n  Cookies.set(AUTH_COOKIE, authUser.jwt);\n  return authUser;\n};\n\nconst initializeUser = async (user: any) => {\n  if (typeof user === 'undefined') {\n    const newUser = await createUser();\n    return loginAsUser(newUser);\n  } else if (user) {\n    return loginAsUser(user);\n  } else {\n    return null;\n  }\n};\n\nexport const renderApp = async (\n  ui: any,\n  { user, ...renderOptions }: Record<string, any> = {},\n) => {\n  // if you want to render the app unauthenticated then pass \"null\" as the user\n  const initializedUser = await initializeUser(user);\n\n  const returnValue = {\n    ...rtlRender(ui, {\n      wrapper: AppProvider,\n      ...renderOptions,\n    }),\n    user: initializedUser,\n  };\n\n  return returnValue;\n};\n\nexport * from '@testing-library/react';\nexport { userEvent, rtlRender };\n"
  },
  {
    "path": "apps/nextjs-app/src/types/api.ts",
    "content": "// let's imagine this file is autogenerated from the backend\n// ideally, we want to keep these api related types in sync\n// with the backend instead of manually writing them out\n\nexport type BaseEntity = {\n  id: string;\n  createdAt: number;\n};\n\nexport type Entity<T> = {\n  [K in keyof T]: T[K];\n} & BaseEntity;\n\nexport type Meta = {\n  page: number;\n  total: number;\n  totalPages: number;\n};\n\nexport type User = Entity<{\n  firstName: string;\n  lastName: string;\n  email: string;\n  role: 'ADMIN' | 'USER';\n  teamId: string;\n  bio: string;\n}>;\n\nexport type AuthResponse = {\n  jwt: string;\n  user: User;\n};\n\nexport type Team = Entity<{\n  name: string;\n  description: string;\n}>;\n\nexport type Discussion = Entity<{\n  title: string;\n  body: string;\n  teamId: string;\n  author: User;\n  public: boolean;\n}>;\n\nexport type Comment = Entity<{\n  body: string;\n  discussionId: string;\n  author: User;\n}>;\n"
  },
  {
    "path": "apps/nextjs-app/src/utils/auth.ts",
    "content": "import { cookies } from 'next/headers';\n\nexport const AUTH_TOKEN_COOKIE_NAME = 'bulletproof_react_app_token';\n\nexport const getAuthTokenCookie = () => {\n  if (typeof window !== 'undefined') return '';\n  const cookieStore = cookies();\n  return cookieStore.get(AUTH_TOKEN_COOKIE_NAME)?.value;\n};\n\nexport const checkLoggedIn = () => {\n  const cookieStore = cookies();\n  const isLoggedIn = !!cookieStore.get(AUTH_TOKEN_COOKIE_NAME);\n  return isLoggedIn;\n};\n"
  },
  {
    "path": "apps/nextjs-app/src/utils/cn.ts",
    "content": "import { type ClassValue, clsx } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"
  },
  {
    "path": "apps/nextjs-app/src/utils/format.ts",
    "content": "import { default as dayjs } from 'dayjs';\n\nexport const formatDate = (date: number) =>\n  dayjs(date).format('MMMM D, YYYY h:mm A');\n"
  },
  {
    "path": "apps/nextjs-app/tailwind.config.cjs",
    "content": "/** @type {import('tailwindcss').Config} */\n\nconst defaultTheme = require('tailwindcss/defaultTheme');\n\nmodule.exports = {\n  content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],\n  theme: {\n    container: {\n      center: true,\n      padding: '2rem',\n      screens: {\n        '2xl': '1400px',\n      },\n    },\n    extend: {\n      fontFamily: {\n        sans: ['Inter var', ...defaultTheme.fontFamily.sans],\n      },\n      colors: {\n        border: 'hsl(var(--border))',\n        input: 'hsl(var(--input))',\n        ring: 'hsl(var(--ring))',\n        background: 'hsl(var(--background))',\n        foreground: 'hsl(var(--foreground))',\n        primary: {\n          DEFAULT: 'hsl(var(--primary))',\n          foreground: 'hsl(var(--primary-foreground))',\n        },\n        secondary: {\n          DEFAULT: 'hsl(var(--secondary))',\n          foreground: 'hsl(var(--secondary-foreground))',\n        },\n        destructive: {\n          DEFAULT: 'hsl(var(--destructive))',\n          foreground: 'hsl(var(--destructive-foreground))',\n        },\n        muted: {\n          DEFAULT: 'hsl(var(--muted))',\n          foreground: 'hsl(var(--muted-foreground))',\n        },\n        accent: {\n          DEFAULT: 'hsl(var(--accent))',\n          foreground: 'hsl(var(--accent-foreground))',\n        },\n        popover: {\n          DEFAULT: 'hsl(var(--popover))',\n          foreground: 'hsl(var(--popover-foreground))',\n        },\n        card: {\n          DEFAULT: 'hsl(var(--card))',\n          foreground: 'hsl(var(--card-foreground))',\n        },\n      },\n      borderRadius: {\n        lg: 'var(--radius)',\n        md: 'calc(var(--radius) - 2px)',\n        sm: 'calc(var(--radius) - 4px)',\n      },\n      keyframes: {\n        'accordion-down': {\n          from: { height: '0' },\n          to: { height: 'var(--radix-accordion-content-height)' },\n        },\n        'accordion-up': {\n          from: { height: 'var(--radix-accordion-content-height)' },\n          to: { height: '0' },\n        },\n      },\n      animation: {\n        'accordion-down': 'accordion-down 0.2s ease-out',\n        'accordion-up': 'accordion-up 0.2s ease-out',\n      },\n    },\n  },\n  plugins: [require('tailwindcss-animate'), require('@tailwindcss/typography')],\n};\n"
  },
  {
    "path": "apps/nextjs-app/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"target\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"incremental\": true,\n    \"paths\": {\n      \"@/*\": [\"./src/*\"]\n    },\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ]\n  },\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".next/types/**/*.ts\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "apps/nextjs-app/vitest.config.ts",
    "content": "/// <reference types=\"vitest\" />\n\nimport react from '@vitejs/plugin-react';\nimport viteTsconfigPaths from 'vite-tsconfig-paths';\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  base: './',\n  plugins: [react(), viteTsconfigPaths()],\n  test: {\n    globals: true,\n    environment: 'jsdom',\n    setupFiles: './src/testing/setup-tests.ts',\n    exclude: ['**/node_modules/**', '**/e2e/**'],\n    coverage: {\n      include: ['src/**'],\n    },\n  },\n});\n"
  },
  {
    "path": "apps/nextjs-pages/.eslintrc.cjs",
    "content": "module.exports = {\n  root: true,\n  env: {\n    node: true,\n    es6: true,\n  },\n  parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },\n  ignorePatterns: [\n    'node_modules/*',\n    'public/mockServiceWorker.js',\n    'generators/*',\n  ],\n  extends: ['eslint:recommended', 'next/core-web-vitals'],\n  plugins: ['check-file'],\n  overrides: [\n    {\n      files: ['**/*.ts', '**/*.tsx'],\n      parser: '@typescript-eslint/parser',\n      settings: {\n        react: { version: 'detect' },\n        'import/resolver': {\n          typescript: {},\n        },\n      },\n      env: {\n        browser: true,\n        node: true,\n        es6: true,\n      },\n      extends: [\n        'eslint:recommended',\n        'plugin:import/errors',\n        'plugin:import/warnings',\n        'plugin:import/typescript',\n        'plugin:@typescript-eslint/recommended',\n        'plugin:react/recommended',\n        'plugin:react-hooks/recommended',\n        'plugin:jsx-a11y/recommended',\n        'plugin:prettier/recommended',\n        'plugin:testing-library/react',\n        'plugin:jest-dom/recommended',\n        'plugin:tailwindcss/recommended',\n        'plugin:vitest/legacy-recommended',\n      ],\n      rules: {\n        '@next/next/no-img-element': 'off',\n        'import/no-restricted-paths': [\n          'error',\n          {\n            zones: [\n              // disables cross-feature imports:\n              // eg. src/features/discussions should not import from src/features/comments, etc.\n              {\n                target: './src/features/auth',\n                from: './src/features',\n                except: ['./auth'],\n              },\n              {\n                target: './src/features/comments',\n                from: './src/features',\n                except: ['./comments'],\n              },\n              {\n                target: './src/features/discussions',\n                from: './src/features',\n                except: ['./discussions'],\n              },\n              {\n                target: './src/features/teams',\n                from: './src/features',\n                except: ['./teams'],\n              },\n              {\n                target: './src/features/users',\n                from: './src/features',\n                except: ['./users'],\n              },\n              // enforce unidirectional codebase:\n\n              // e.g. src/app can import from src/features but not the other way around\n              {\n                target: './src/features',\n                from: './src/app',\n              },\n\n              // e.g src/features and src/app can import from these shared modules but not the other way around\n              {\n                target: [\n                  './src/components',\n                  './src/hooks',\n                  './src/lib',\n                  './src/types',\n                  './src/utils',\n                ],\n                from: ['./src/features', './src/app'],\n              },\n            ],\n          },\n        ],\n        'import/no-cycle': 'error',\n        'linebreak-style': ['error', 'unix'],\n        'react/prop-types': 'off',\n        'import/order': [\n          'error',\n          {\n            groups: [\n              'builtin',\n              'external',\n              'internal',\n              'parent',\n              'sibling',\n              'index',\n              'object',\n            ],\n            'newlines-between': 'always',\n            alphabetize: { order: 'asc', caseInsensitive: true },\n          },\n        ],\n        'import/default': 'off',\n        'import/no-named-as-default-member': 'off',\n        'import/no-named-as-default': 'off',\n        'react/react-in-jsx-scope': 'off',\n        'jsx-a11y/anchor-is-valid': 'off',\n        '@typescript-eslint/no-unused-vars': ['error'],\n        '@typescript-eslint/explicit-function-return-type': ['off'],\n        '@typescript-eslint/explicit-module-boundary-types': ['off'],\n        '@typescript-eslint/no-empty-function': ['off'],\n        '@typescript-eslint/no-explicit-any': ['off'],\n        'prettier/prettier': ['error', {}, { usePrettierrc: true }],\n        'check-file/filename-naming-convention': [\n          'error',\n          {\n            'src/!(pages)/*.{ts,tsx}': 'KEBAB_CASE',\n          },\n          {\n            ignoreMiddleExtensions: true,\n          },\n        ],\n      },\n    },\n    {\n      plugins: ['check-file'],\n      files: ['src/**/!(__tests__)/*'],\n      rules: {\n        'check-file/folder-naming-convention': [\n          'error',\n          {\n            '**/*': 'KEBAB_CASE',\n          },\n        ],\n      },\n    },\n  ],\n};\n"
  },
  {
    "path": "apps/nextjs-pages/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n/test-results/\n/playwright-report/\n/blob-report/\n/playwright/.cache/\n/e2e/.auth/\n\n# storybook\nmigration-storybook.log\nstorybook.log\nstorybook-static\n\n\n# production\n/dist\n\n# misc\n.DS_Store\n.env\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n\n# local\nmocked-db.json\n\n/.next\n/.vite\ntsconfig.tsbuildinfo"
  },
  {
    "path": "apps/nextjs-pages/.prettierignore",
    "content": "*.hbs"
  },
  {
    "path": "apps/nextjs-pages/.prettierrc",
    "content": "{\n  \"singleQuote\": true,\n  \"trailingComma\": \"all\",\n  \"printWidth\": 80,\n  \"tabWidth\": 2,\n  \"useTabs\": false\n}\n"
  },
  {
    "path": "apps/nextjs-pages/.storybook/main.ts",
    "content": "module.exports = {\n  stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],\n\n  addons: [\n    '@storybook/addon-actions',\n    '@storybook/addon-links',\n    '@storybook/node-logger',\n    '@storybook/addon-essentials',\n    '@storybook/addon-interactions',\n    '@storybook/addon-docs',\n    '@storybook/addon-a11y',\n  ],\n  framework: '@storybook/nextjs',\n  docs: {\n    autodocs: 'tag',\n  },\n  typescript: {\n    reactDocgen: 'react-docgen-typescript',\n  },\n};\n"
  },
  {
    "path": "apps/nextjs-pages/.storybook/preview.tsx",
    "content": "import React from 'react';\nimport '../src/styles/globals.css';\n\nexport const parameters = {\n  actions: { argTypesRegex: '^on[A-Z].*' },\n};\n\nexport const decorators = [(Story) => <Story />];\n"
  },
  {
    "path": "apps/nextjs-pages/.vscode/extensions.json",
    "content": "{\n  \"recommendations\": [\n    \"dbaeumer.vscode-eslint\",\n    \"esbenp.prettier-vscode\",\n    \"dsznajder.es7-react-js-snippets\",\n    \"mariusalchimavicius.json-to-ts\",\n    \"bradlc.vscode-tailwindcss\"\n  ]\n}\n"
  },
  {
    "path": "apps/nextjs-pages/.vscode/settings.json",
    "content": "{\n  \"editor.formatOnSave\": true,\n  \"editor.codeActionsOnSave\": {\n    \"source.fixAll.eslint\": \"explicit\"\n  }\n}\n"
  },
  {
    "path": "apps/nextjs-pages/README.md",
    "content": "# 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 following commands.\n\n```bash\ngit clone https://github.com/alan2207/bulletproof-react.git\ncd bulletproof-react\ncd apps/nextjs-pages\ncp .env.example .env\nyarn install\n```\n\n#### `yarn run-mock-server`\n\nMake sure to start the mock server before running the app.\nThe mock server runs on [http://localhost:8080/api](http://localhost:8080/api).\n\n##### `yarn dev`\n\nRuns the app in the development mode.\\\nOpen [http://localhost:3000](http://localhost:3000) to view it in the browser.\n\n## Project Structure\n\nSince the `pages` folder isn't very flexible and doesn't allow file collocation, we are keeping the `app` folder which is our application layer where we compose all the features, and then we just re-export Next.js page specific files (the pages and `getServerSideProps`) from the `pages` folder so Next.js can pick them up and serve as pages.\n"
  },
  {
    "path": "apps/nextjs-pages/__mocks__/vitest-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n/// <reference types=\"vitest/globals\" />\n"
  },
  {
    "path": "apps/nextjs-pages/__mocks__/zustand.ts",
    "content": "import { act } from '@testing-library/react';\nimport { afterEach, vi } from 'vitest';\nimport * as zustand from 'zustand';\n\nconst { create: actualCreate, createStore: actualCreateStore } =\n  await vi.importActual<typeof zustand>('zustand');\n\n// a variable to hold reset functions for all stores declared in the app\nexport const storeResetFns = new Set<() => void>();\n\nconst createUncurried = <T>(stateCreator: zustand.StateCreator<T>) => {\n  const store = actualCreate(stateCreator);\n  const initialState = store.getInitialState();\n  storeResetFns.add(() => {\n    store.setState(initialState, true);\n  });\n  return store;\n};\n\n// when creating a store, we get its initial state, create a reset function and add it in the set\nexport const create = (<T>(stateCreator: zustand.StateCreator<T>) => {\n  // to support curried version of create\n  return typeof stateCreator === 'function'\n    ? createUncurried(stateCreator)\n    : createUncurried;\n}) as typeof zustand.create;\n\nconst createStoreUncurried = <T>(stateCreator: zustand.StateCreator<T>) => {\n  const store = actualCreateStore(stateCreator);\n  const initialState = store.getInitialState();\n  storeResetFns.add(() => {\n    store.setState(initialState, true);\n  });\n  return store;\n};\n\n// when creating a store, we get its initial state, create a reset function and add it in the set\nexport const createStore = (<T>(stateCreator: zustand.StateCreator<T>) => {\n  // to support curried version of createStore\n  return typeof stateCreator === 'function'\n    ? createStoreUncurried(stateCreator)\n    : createStoreUncurried;\n}) as typeof zustand.createStore;\n\n// reset all stores after each test run\nafterEach(() => {\n  act(() => {\n    storeResetFns.forEach((resetFn) => {\n      resetFn();\n    });\n  });\n});\n"
  },
  {
    "path": "apps/nextjs-pages/e2e/.eslintrc.cjs",
    "content": "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",
    "content": "import { test as setup, expect } from '@playwright/test';\nimport { createUser } from '../../src/testing/data-generators';\n\nconst authFile = 'e2e/.auth/user.json';\n\nsetup('authenticate', async ({ page }) => {\n  const user = createUser();\n\n  await page.goto('/');\n  await page.getByRole('button', { name: 'Get started' }).click();\n  await page.waitForURL('/auth/login');\n  await page.getByRole('link', { name: 'Register' }).click();\n\n  // registration:\n  await page.getByLabel('First Name').click();\n  await page.getByLabel('First Name').fill(user.firstName);\n  await page.getByLabel('Last Name').click();\n  await page.getByLabel('Last Name').fill(user.lastName);\n  await page.getByLabel('Email Address').click();\n  await page.getByLabel('Email Address').fill(user.email);\n  await page.getByLabel('Password').click();\n  await page.getByLabel('Password').fill(user.password);\n  await page.getByLabel('Team Name').click();\n  await page.getByLabel('Team Name').fill(user.teamName);\n  await page.getByRole('button', { name: 'Register' }).click();\n  await page.waitForURL('/app');\n\n  // log out:\n  await page.getByRole('button', { name: 'Open user menu' }).click();\n  await page.getByRole('menuitem', { name: 'Sign Out' }).click();\n  await page.waitForURL('/auth/login?redirectTo=%2Fapp');\n\n  // log in:\n  await page.getByLabel('Email Address').click();\n  await page.getByLabel('Email Address').fill(user.email);\n  await page.getByLabel('Password').click();\n  await page.getByLabel('Password').fill(user.password);\n  await page.getByRole('button', { name: 'Log in' }).click();\n  await page.waitForURL('/app');\n\n  await page.context().storageState({ path: authFile });\n});\n"
  },
  {
    "path": "apps/nextjs-pages/e2e/tests/profile.spec.ts",
    "content": "import { test, expect } from '@playwright/test';\n\ntest('profile', async ({ page }) => {\n  // update user:\n  await page.goto('/app');\n  await page.getByRole('button', { name: 'Open user menu' }).click();\n  await page.getByRole('menuitem', { name: 'Your Profile' }).click();\n  await page.getByRole('button', { name: 'Update Profile' }).click();\n  await page.getByLabel('Bio').click();\n  await page.getByLabel('Bio').fill('My bio');\n  await page.getByRole('button', { name: 'Submit' }).click();\n  await page\n    .getByLabel('Profile Updated')\n    .getByRole('button', { name: 'Close' })\n    .click();\n  await expect(page.getByText('My bio')).toBeVisible();\n});\n"
  },
  {
    "path": "apps/nextjs-pages/e2e/tests/smoke.spec.ts",
    "content": "import { test, expect } from '@playwright/test';\n\nimport {\n  createDiscussion,\n  createComment,\n} from '../../src/testing/data-generators';\ntest('smoke', async ({ page }) => {\n  const discussion = createDiscussion();\n  const comment = createComment();\n\n  await page.goto('/');\n  await page.getByRole('button', { name: 'Get started' }).click();\n  await page.waitForURL('/app');\n\n  // create discussion:\n  await page.getByRole('link', { name: 'Discussions' }).click();\n  await page.waitForURL('/app/discussions');\n\n  await page.getByRole('button', { name: 'Create Discussion' }).click();\n  await page.getByLabel('Title').click();\n  await page.getByLabel('Title').fill(discussion.title);\n  await page.getByLabel('Body').click();\n  await page.getByLabel('Body').fill(discussion.body);\n  await page.getByRole('button', { name: 'Submit' }).click();\n  await page\n    .getByLabel('Discussion Created')\n    .getByRole('button', { name: 'Close' })\n    .click();\n\n  // visit discussion page:\n  await page.getByRole('link', { name: 'View' }).click();\n\n  await expect(\n    page.getByRole('heading', { name: discussion.title }),\n  ).toBeVisible();\n  await expect(page.getByText(discussion.body)).toBeVisible();\n\n  // update discussion:\n  await page.getByRole('button', { name: 'Update Discussion' }).click();\n  await page.getByLabel('Title').click();\n  await page.getByLabel('Title').fill(`${discussion.title} - updated`);\n  await page.getByLabel('Body').click();\n  await page.getByLabel('Body').fill(`${discussion.body} - updated`);\n  await page.getByRole('button', { name: 'Submit' }).click();\n  await page\n    .getByLabel('Discussion Updated')\n    .getByRole('button', { name: 'Close' })\n    .click();\n\n  await expect(\n    page.getByRole('heading', { name: `${discussion.title} - updated` }),\n  ).toBeVisible();\n  await expect(page.getByText(`${discussion.body} - updated`)).toBeVisible();\n\n  // create comment:\n  await page.getByRole('button', { name: 'Create Comment' }).click();\n  await page.getByLabel('Body').click();\n  await page.getByLabel('Body').fill(comment.body);\n  await page.getByRole('button', { name: 'Submit' }).click();\n  await expect(page.getByText(comment.body)).toBeVisible();\n  await page\n    .getByLabel('Comment Created')\n    .getByRole('button', { name: 'Close' })\n    .click();\n\n  // delete comment:\n  await page.getByRole('button', { name: 'Delete Comment' }).click();\n  await expect(\n    page.getByText('Are you sure you want to delete this comment?'),\n  ).toBeVisible();\n  await page.getByRole('button', { name: 'Delete Comment' }).click();\n  await page\n    .getByLabel('Comment Deleted')\n    .getByRole('button', { name: 'Close' })\n    .click();\n  await expect(\n    page.getByRole('heading', { name: 'No Comments Found' }),\n  ).toBeVisible();\n  await expect(page.getByText(comment.body)).toBeHidden();\n\n  // go back to discussions:\n  await page.getByRole('link', { name: 'Discussions' }).click();\n  await page.waitForURL('/app/discussions');\n\n  // delete discussion:\n  await page.getByRole('button', { name: 'Delete Discussion' }).click();\n  await page.getByRole('button', { name: 'Delete Discussion' }).click();\n  await page\n    .getByLabel('Discussion Deleted')\n    .getByRole('button', { name: 'Close' })\n    .click();\n  await expect(\n    page.getByRole('heading', { name: 'No Entries Found' }),\n  ).toBeVisible();\n});\n"
  },
  {
    "path": "apps/nextjs-pages/generators/component/component.stories.tsx.hbs",
    "content": "import { Meta, StoryObj } from '@storybook/react';\n\nimport { {{ properCase name }} } from './{{ kebabCase name }}';\n\nconst meta: Meta<typeof {{ properCase name }}> = {\n  component: {{ properCase name }},\n};\n\nexport default meta;\n\ntype Story = StoryObj<typeof {{ properCase name }}>;\n\nexport const Default: Story = {\n  args: {}\n};\n"
  },
  {
    "path": "apps/nextjs-pages/generators/component/component.tsx.hbs",
    "content": "import * as React from \"react\"; \n\nexport type {{properCase name}}Props = {};\n\nexport const {{properCase name}} = (props: {{properCase name}}Props) => { \n  return (\n    <div>\n      {{properCase name}}\n    </div>\n  ); \n};"
  },
  {
    "path": "apps/nextjs-pages/generators/component/index.cjs",
    "content": "const path = require('path');\nconst fs = require('fs');\n\nconst featuresDir = path.join(process.cwd(), 'src/features');\nconst features = fs.readdirSync(featuresDir);\n\n/**\n *\n * @type {import('plop').PlopGenerator}\n */\nmodule.exports = {\n  description: 'Component Generator',\n  prompts: [\n    {\n      type: 'input',\n      name: 'name',\n      message: 'component name',\n    },\n    {\n      type: 'list',\n      name: 'feature',\n      message: 'Which feature does this component belong to?',\n      choices: ['components', ...features],\n      when: () => features.length > 0,\n    },\n    {\n      type: 'input',\n      name: 'folder',\n      message: 'folder in components',\n      when: ({ feature }) => !feature || feature === 'components',\n    },\n  ],\n  actions: (answers) => {\n    const componentGeneratePath =\n      !answers.feature || answers.feature === 'components'\n        ? 'src/components/{{folder}}'\n        : 'src/features/{{feature}}/components';\n    return [\n      {\n        type: 'add',\n        path: componentGeneratePath + '/{{kebabCase name}}/index.ts',\n        templateFile: 'generators/component/index.ts.hbs',\n      },\n      {\n        type: 'add',\n        path: componentGeneratePath + '/{{kebabCase name}}/{{kebabCase name}}.tsx',\n        templateFile: 'generators/component/component.tsx.hbs',\n      },\n      {\n        type: 'add',\n        path: componentGeneratePath + '/{{kebabCase name}}/{{kebabCase name}}.stories.tsx',\n        templateFile: 'generators/component/component.stories.tsx.hbs',\n      },\n    ];\n  },\n};\n"
  },
  {
    "path": "apps/nextjs-pages/generators/component/index.ts.hbs",
    "content": "export * from './{{ kebabCase name }}';\n"
  },
  {
    "path": "apps/nextjs-pages/lint-staged.config.mjs",
    "content": "import path from 'path';\n\nconst buildEslintCommand = (filenames) => {\n  return `next lint --fix --file ${filenames\n    .filter((f) => f.includes('/src/'))\n    .map((f) => path.relative(process.cwd(), f))\n    .join(' --file ')}`;\n};\n\nconst config = {\n  '*.{ts,tsx}': [buildEslintCommand, \"bash -c 'yarn check-types'\"],\n};\n\nexport default config;\n"
  },
  {
    "path": "apps/nextjs-pages/mock-server.ts",
    "content": "import { createMiddleware } from '@mswjs/http-middleware';\nimport cors from 'cors';\nimport express from 'express';\nimport logger from 'pino-http';\n\nimport { initializeDb } from './src/testing/mocks/db';\nimport { handlers } from './src/testing/mocks/handlers';\n\nconst app = express();\n\napp.use(\n  cors({\n    origin: process.env.NEXT_PUBLIC_URL,\n    credentials: true,\n  }),\n);\n\napp.use(express.json());\napp.use(logger({ level: 'silent' }));\napp.use(createMiddleware(...handlers));\n\ninitializeDb().then(() => {\n  console.log('Mock DB initialized');\n  app.listen(process.env.NEXT_PUBLIC_MOCK_API_PORT, () => {\n    console.log(\n      `Mock API server started at http://localhost:${process.env.NEXT_PUBLIC_MOCK_API_PORT}`,\n    );\n  });\n});\n"
  },
  {
    "path": "apps/nextjs-pages/next-env.d.ts",
    "content": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\n/// <reference types=\"next/navigation-types/compat/navigation\" />\n\n// NOTE: This file should not be edited\n// see https://nextjs.org/docs/basic-features/typescript for more information.\n"
  },
  {
    "path": "apps/nextjs-pages/next.config.mjs",
    "content": "/** @type {import('next').NextConfig} */\nconst nextConfig = {\n  reactStrictMode: true,\n};\n\nexport default nextConfig;\n"
  },
  {
    "path": "apps/nextjs-pages/package.json",
    "content": "{\n  \"name\": \"bulletproof-react-nextjs-pages\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"lint\": \"next lint\",\n    \"test\": \"vitest\",\n    \"test-e2e\": \"pm2 start \\\"yarn run-mock-server\\\" --name server && yarn playwright test\",\n    \"prepare\": \"husky\",\n    \"check-types\": \"tsc --project tsconfig.json --pretty --noEmit\",\n    \"generate\": \"plop\",\n    \"storybook\": \"storybook dev -p 6006\",\n    \"build-storybook\": \"storybook build\",\n    \"run-mock-server\": \"tsx ./mock-server.ts\"\n  },\n  \"dependencies\": {\n    \"@hookform/resolvers\": \"^3.3.4\",\n    \"@next/env\": \"^14.2.5\",\n    \"@ngneat/falso\": \"^7.2.0\",\n    \"@radix-ui/react-dialog\": \"^1.0.5\",\n    \"@radix-ui/react-dropdown-menu\": \"^2.0.6\",\n    \"@radix-ui/react-icons\": \"^1.3.0\",\n    \"@radix-ui/react-label\": \"^2.0.2\",\n    \"@radix-ui/react-slot\": \"^1.0.2\",\n    \"@radix-ui/react-switch\": \"^1.0.3\",\n    \"@tanstack/react-query\": \"^5.32.0\",\n    \"@tanstack/react-query-devtools\": \"^5.32.0\",\n    \"axios\": \"^1.6.8\",\n    \"class-variance-authority\": \"^0.7.0\",\n    \"clsx\": \"^2.1.1\",\n    \"dayjs\": \"^1.11.11\",\n    \"eslint-plugin-check-file\": \"^2.8.0\",\n    \"isomorphic-dompurify\": \"^2.14.0\",\n    \"lucide-react\": \"^0.378.0\",\n    \"marked\": \"^12.0.2\",\n    \"nanoid\": \"^5.0.7\",\n    \"next\": \"^14.2.5\",\n    \"react\": \"^18.3.1\",\n    \"react-dom\": \"^18.3.1\",\n    \"react-error-boundary\": \"^4.0.13\",\n    \"react-hook-form\": \"^7.51.3\",\n    \"react-query-auth\": \"^2.3.0\",\n    \"tailwind-merge\": \"^2.3.0\",\n    \"tailwindcss-animate\": \"^1.0.7\",\n    \"zod\": \"^3.23.4\",\n    \"zustand\": \"^4.5.2\"\n  },\n  \"devDependencies\": {\n    \"@eslint/eslintrc\": \"^3.0.2\",\n    \"@mswjs/data\": \"^0.16.1\",\n    \"@mswjs/http-middleware\": \"^0.10.1\",\n    \"@playwright/test\": \"^1.43.1\",\n    \"@storybook/addon-a11y\": \"^8.0.10\",\n    \"@storybook/addon-actions\": \"^8.0.9\",\n    \"@storybook/addon-essentials\": \"^8.0.9\",\n    \"@storybook/addon-links\": \"^8.0.9\",\n    \"@storybook/nextjs\": \"^8.2.9\",\n    \"@storybook/node-logger\": \"^8.0.9\",\n    \"@storybook/react\": \"^8.0.9\",\n    \"@tailwindcss/typography\": \"^0.5.13\",\n    \"@testing-library/jest-dom\": \"^6.4.2\",\n    \"@testing-library/react\": \"^15.0.5\",\n    \"@testing-library/user-event\": \"^14.5.2\",\n    \"@types/cors\": \"^2.8.17\",\n    \"@types/dompurify\": \"^3.0.5\",\n    \"@types/js-cookie\": \"^3.0.6\",\n    \"@types/marked\": \"^6.0.0\",\n    \"@types/node\": \"^20.12.7\",\n    \"@types/react\": \"^18.3.1\",\n    \"@types/react-dom\": \"^18.3.0\",\n    \"@typescript-eslint/eslint-plugin\": \"^7.8.0\",\n    \"@typescript-eslint/parser\": \"^7.8.0\",\n    \"@vitejs/plugin-react\": \"^4.2.1\",\n    \"autoprefixer\": \"^10.4.19\",\n    \"cors\": \"^2.8.5\",\n    \"dotenv\": \"^16.4.5\",\n    \"eslint\": \"8\",\n    \"eslint-config-next\": \"^14.2.5\",\n    \"eslint-config-prettier\": \"^9.1.0\",\n    \"eslint-import-resolver-typescript\": \"^3.6.1\",\n    \"eslint-plugin-import\": \"^2.29.1\",\n    \"eslint-plugin-jest-dom\": \"^5.4.0\",\n    \"eslint-plugin-jsx-a11y\": \"^6.8.0\",\n    \"eslint-plugin-playwright\": \"^1.6.0\",\n    \"eslint-plugin-prettier\": \"^5.1.3\",\n    \"eslint-plugin-react\": \"^7.34.1\",\n    \"eslint-plugin-react-hooks\": \"^4.6.2\",\n    \"eslint-plugin-tailwindcss\": \"^3.15.1\",\n    \"eslint-plugin-testing-library\": \"^6.2.2\",\n    \"eslint-plugin-vitest\": \"^0.5.4\",\n    \"express\": \"^4.19.2\",\n    \"husky\": \"^9.0.11\",\n    \"jest-environment-jsdom\": \"^29.7.0\",\n    \"js-cookie\": \"^3.0.5\",\n    \"jsdom\": \"^24.0.0\",\n    \"lint-staged\": \"^15.2.2\",\n    \"msw\": \"^2.2.14\",\n    \"next-router-mock\": \"^0.9.13\",\n    \"pino-http\": \"^10.1.0\",\n    \"pino-pretty\": \"^11.1.0\",\n    \"plop\": \"^4.0.1\",\n    \"pm2\": \"^5.4.0\",\n    \"postcss\": \"^8.4.38\",\n    \"prettier\": \"^3.2.5\",\n    \"storybook\": \"^8.0.9\",\n    \"tailwindcss\": \"^3.4.3\",\n    \"tsx\": \"^4.17.0\",\n    \"typescript\": \"^5.4.5\",\n    \"vite-tsconfig-paths\": \"^4.3.2\",\n    \"vitest\": \"^2.1.4\"\n  },\n  \"msw\": {\n    \"workerDirectory\": \"public\"\n  }\n}\n"
  },
  {
    "path": "apps/nextjs-pages/playwright.config.ts",
    "content": "import { defineConfig, devices } from '@playwright/test';\n\nconst PORT = 3000;\n\n/**\n * Read environment variables from file.\n * https://github.com/motdotla/dotenv\n */\n// require('dotenv').config();\n\n/**\n * See https://playwright.dev/docs/test-configuration.\n */\nexport default defineConfig({\n  testDir: './e2e',\n  /* Run tests in files in parallel */\n  fullyParallel: true,\n  /* Fail the build on CI if you accidentally left test.only in the source code. */\n  forbidOnly: !!process.env.CI,\n  /* Retry on CI only */\n  retries: process.env.CI ? 2 : 0,\n  /* Opt out of parallel tests on CI. */\n  workers: process.env.CI ? 1 : undefined,\n  /* Reporter to use. See https://playwright.dev/docs/test-reporters */\n  reporter: 'html',\n  /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */\n  use: {\n    /* Base URL to use in actions like `await page.goto('/')`. */\n    // baseURL: 'http://127.0.0.1:3000',\n\n    /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */\n    trace: 'on-first-retry',\n  },\n\n  /* Configure projects for major browsers */\n  projects: [\n    { name: 'setup', testMatch: /.*\\.setup\\.ts/ },\n    {\n      name: 'chromium',\n      testMatch: /.*\\.spec\\.ts/,\n      use: {\n        ...devices['Desktop Chrome'],\n        storageState: 'e2e/.auth/user.json',\n      },\n      dependencies: ['setup'],\n    },\n  ],\n\n  /* Run your local dev server before starting the tests */\n  webServer: {\n    command: `yarn dev --port ${PORT}`,\n    timeout: 10 * 1000,\n    port: PORT,\n    reuseExistingServer: !process.env.CI,\n  },\n});\n"
  },
  {
    "path": "apps/nextjs-pages/plopfile.cjs",
    "content": "const componentGenerator = require('./generators/component/index');\n\n/**\n *\n * @param {import('plop').NodePlopAPI} plop\n */\nmodule.exports = function (plop) {\n  plop.setGenerator('component', componentGenerator);\n};\n"
  },
  {
    "path": "apps/nextjs-pages/postcss.config.cjs",
    "content": "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n};\n"
  },
  {
    "path": "apps/nextjs-pages/public/_redirects",
    "content": "/* /index.html 200"
  },
  {
    "path": "apps/nextjs-pages/public/mockServiceWorker.js",
    "content": "/* eslint-disable */\n/* tslint:disable */\n\n/**\n * Mock Service Worker.\n * @see https://github.com/mswjs/msw\n * - Please do NOT modify this file.\n * - Please do NOT serve this file on production.\n */\n\nconst PACKAGE_VERSION = '2.3.5'\nconst INTEGRITY_CHECKSUM = '26357c79639bfa20d64c0efca2a87423'\nconst IS_MOCKED_RESPONSE = Symbol('isMockedResponse')\nconst activeClientIds = new Set()\n\nself.addEventListener('install', function () {\n  self.skipWaiting()\n})\n\nself.addEventListener('activate', function (event) {\n  event.waitUntil(self.clients.claim())\n})\n\nself.addEventListener('message', async function (event) {\n  const clientId = event.source.id\n\n  if (!clientId || !self.clients) {\n    return\n  }\n\n  const client = await self.clients.get(clientId)\n\n  if (!client) {\n    return\n  }\n\n  const allClients = await self.clients.matchAll({\n    type: 'window',\n  })\n\n  switch (event.data) {\n    case 'KEEPALIVE_REQUEST': {\n      sendToClient(client, {\n        type: 'KEEPALIVE_RESPONSE',\n      })\n      break\n    }\n\n    case 'INTEGRITY_CHECK_REQUEST': {\n      sendToClient(client, {\n        type: 'INTEGRITY_CHECK_RESPONSE',\n        payload: {\n          packageVersion: PACKAGE_VERSION,\n          checksum: INTEGRITY_CHECKSUM,\n        },\n      })\n      break\n    }\n\n    case 'MOCK_ACTIVATE': {\n      activeClientIds.add(clientId)\n\n      sendToClient(client, {\n        type: 'MOCKING_ENABLED',\n        payload: true,\n      })\n      break\n    }\n\n    case 'MOCK_DEACTIVATE': {\n      activeClientIds.delete(clientId)\n      break\n    }\n\n    case 'CLIENT_CLOSED': {\n      activeClientIds.delete(clientId)\n\n      const remainingClients = allClients.filter((client) => {\n        return client.id !== clientId\n      })\n\n      // Unregister itself when there are no more clients\n      if (remainingClients.length === 0) {\n        self.registration.unregister()\n      }\n\n      break\n    }\n  }\n})\n\nself.addEventListener('fetch', function (event) {\n  const { request } = event\n\n  // Bypass navigation requests.\n  if (request.mode === 'navigate') {\n    return\n  }\n\n  // Opening the DevTools triggers the \"only-if-cached\" request\n  // that cannot be handled by the worker. Bypass such requests.\n  if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {\n    return\n  }\n\n  // Bypass all requests when there are no active clients.\n  // Prevents the self-unregistered worked from handling requests\n  // after it's been deleted (still remains active until the next reload).\n  if (activeClientIds.size === 0) {\n    return\n  }\n\n  // Generate unique request ID.\n  const requestId = crypto.randomUUID()\n  event.respondWith(handleRequest(event, requestId))\n})\n\nasync function handleRequest(event, requestId) {\n  const client = await resolveMainClient(event)\n  const response = await getResponse(event, client, requestId)\n\n  // Send back the response clone for the \"response:*\" life-cycle events.\n  // Ensure MSW is active and ready to handle the message, otherwise\n  // this message will pend indefinitely.\n  if (client && activeClientIds.has(client.id)) {\n    ;(async function () {\n      const responseClone = response.clone()\n\n      sendToClient(\n        client,\n        {\n          type: 'RESPONSE',\n          payload: {\n            requestId,\n            isMockedResponse: IS_MOCKED_RESPONSE in response,\n            type: responseClone.type,\n            status: responseClone.status,\n            statusText: responseClone.statusText,\n            body: responseClone.body,\n            headers: Object.fromEntries(responseClone.headers.entries()),\n          },\n        },\n        [responseClone.body],\n      )\n    })()\n  }\n\n  return response\n}\n\n// Resolve the main client for the given event.\n// Client that issues a request doesn't necessarily equal the client\n// that registered the worker. It's with the latter the worker should\n// communicate with during the response resolving phase.\nasync function resolveMainClient(event) {\n  const client = await self.clients.get(event.clientId)\n\n  if (client?.frameType === 'top-level') {\n    return client\n  }\n\n  const allClients = await self.clients.matchAll({\n    type: 'window',\n  })\n\n  return allClients\n    .filter((client) => {\n      // Get only those clients that are currently visible.\n      return client.visibilityState === 'visible'\n    })\n    .find((client) => {\n      // Find the client ID that's recorded in the\n      // set of clients that have registered the worker.\n      return activeClientIds.has(client.id)\n    })\n}\n\nasync function getResponse(event, client, requestId) {\n  const { request } = event\n\n  // Clone the request because it might've been already used\n  // (i.e. its body has been read and sent to the client).\n  const requestClone = request.clone()\n\n  function passthrough() {\n    const headers = Object.fromEntries(requestClone.headers.entries())\n\n    // Remove internal MSW request header so the passthrough request\n    // complies with any potential CORS preflight checks on the server.\n    // Some servers forbid unknown request headers.\n    delete headers['x-msw-intention']\n\n    return fetch(requestClone, { headers })\n  }\n\n  // Bypass mocking when the client is not active.\n  if (!client) {\n    return passthrough()\n  }\n\n  // Bypass initial page load requests (i.e. static assets).\n  // The absence of the immediate/parent client in the map of the active clients\n  // means that MSW hasn't dispatched the \"MOCK_ACTIVATE\" event yet\n  // and is not ready to handle requests.\n  if (!activeClientIds.has(client.id)) {\n    return passthrough()\n  }\n\n  // Notify the client that a request has been intercepted.\n  const requestBuffer = await request.arrayBuffer()\n  const clientMessage = await sendToClient(\n    client,\n    {\n      type: 'REQUEST',\n      payload: {\n        id: requestId,\n        url: request.url,\n        mode: request.mode,\n        method: request.method,\n        headers: Object.fromEntries(request.headers.entries()),\n        cache: request.cache,\n        credentials: request.credentials,\n        destination: request.destination,\n        integrity: request.integrity,\n        redirect: request.redirect,\n        referrer: request.referrer,\n        referrerPolicy: request.referrerPolicy,\n        body: requestBuffer,\n        keepalive: request.keepalive,\n      },\n    },\n    [requestBuffer],\n  )\n\n  switch (clientMessage.type) {\n    case 'MOCK_RESPONSE': {\n      return respondWithMock(clientMessage.data)\n    }\n\n    case 'PASSTHROUGH': {\n      return passthrough()\n    }\n  }\n\n  return passthrough()\n}\n\nfunction sendToClient(client, message, transferrables = []) {\n  return new Promise((resolve, reject) => {\n    const channel = new MessageChannel()\n\n    channel.port1.onmessage = (event) => {\n      if (event.data && event.data.error) {\n        return reject(event.data.error)\n      }\n\n      resolve(event.data)\n    }\n\n    client.postMessage(\n      message,\n      [channel.port2].concat(transferrables.filter(Boolean)),\n    )\n  })\n}\n\nasync function respondWithMock(response) {\n  // Setting response status code to 0 is a no-op.\n  // However, when responding with a \"Response.error()\", the produced Response\n  // instance will have status code set to 0. Since it's not possible to create\n  // a Response instance with status code 0, handle that use-case separately.\n  if (response.status === 0) {\n    return Response.error()\n  }\n\n  const mockedResponse = new Response(response.body, response)\n\n  Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, {\n    value: true,\n    enumerable: true,\n  })\n\n  return mockedResponse\n}\n"
  },
  {
    "path": "apps/nextjs-pages/public/robots.txt",
    "content": "# https://www.robotstxt.org/robotstxt.html\nUser-agent: *\nDisallow:\n"
  },
  {
    "path": "apps/nextjs-pages/src/app/pages/app/dashboard.tsx",
    "content": "import { ReactElement } from 'react';\n\nimport { ContentLayout, DashboardLayout } from '@/components/layouts';\nimport { useUser } from '@/lib/auth';\nimport { ROLES } from '@/lib/authorization';\n\nexport const DashboardPage = () => {\n  const user = useUser();\n  if (!user.data) return null;\n\n  return (\n    <>\n      <h1 className=\"text-xl\">\n        Welcome <b>{`${user.data?.firstName} ${user.data?.lastName}`}</b>\n      </h1>\n      <h4 className=\"my-3\">\n        Your role is : <b>{user.data?.role}</b>\n      </h4>\n      <p className=\"font-medium\">In this application you can:</p>\n      {user.data?.role === ROLES.USER && (\n        <ul className=\"my-4 list-inside list-disc\">\n          <li>Create comments in discussions</li>\n          <li>Delete own comments</li>\n        </ul>\n      )}\n      {user.data?.role === ROLES.ADMIN && (\n        <ul className=\"my-4 list-inside list-disc\">\n          <li>Create discussions</li>\n          <li>Edit discussions</li>\n          <li>Delete discussions</li>\n          <li>Comment on discussions</li>\n          <li>Delete all comments</li>\n        </ul>\n      )}\n    </>\n  );\n};\n\nDashboardPage.getLayout = (page: ReactElement) => {\n  return (\n    <DashboardLayout>\n      <ContentLayout title=\"Dashboard\">{page}</ContentLayout>\n    </DashboardLayout>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/app/pages/app/discussions/__tests__/discussion.test.tsx",
    "content": "import mockRouter from 'next-router-mock';\n\nimport {\n  renderApp,\n  screen,\n  userEvent,\n  waitFor,\n  createDiscussion,\n  createUser,\n  within,\n  waitForLoadingToFinish,\n} from '@/testing/test-utils';\n\nimport { DiscussionPage } from '../discussion';\n\nconst renderDiscussion = async () => {\n  const fakeUser = await createUser();\n  const fakeDiscussion = await createDiscussion({ teamId: fakeUser.teamId });\n\n  mockRouter.query = { discussionId: fakeDiscussion.id };\n\n  const utils = await renderApp(<DiscussionPage />, {\n    user: fakeUser,\n    path: `/app/discussions/:discussionId`,\n    url: `/app/discussions/${fakeDiscussion.id}`,\n  });\n\n  await waitForLoadingToFinish();\n\n  await screen.findByText(fakeDiscussion.title);\n\n  return {\n    ...utils,\n    fakeUser,\n    fakeDiscussion,\n  };\n};\n\ntest('should render discussion', async () => {\n  const { fakeDiscussion } = await renderDiscussion();\n  expect(screen.getByText(fakeDiscussion.body)).toBeInTheDocument();\n});\n\ntest('should update discussion', async () => {\n  const { fakeDiscussion } = await renderDiscussion();\n\n  const titleUpdate = '-Updated';\n  const bodyUpdate = '-Updated';\n\n  await userEvent.click(\n    screen.getByRole('button', { name: /update discussion/i }),\n  );\n\n  const drawer = await screen.findByRole('dialog', {\n    name: /update discussion/i,\n  });\n\n  const titleField = within(drawer).getByText(/title/i);\n  const bodyField = within(drawer).getByText(/body/i);\n\n  const newTitle = `${fakeDiscussion.title}${titleUpdate}`;\n  const newBody = `${fakeDiscussion.body}${bodyUpdate}`;\n\n  // replacing the title with the new title\n  await userEvent.type(titleField, newTitle);\n\n  // appending updated to the body\n  await userEvent.type(bodyField, bodyUpdate);\n\n  const submitButton = within(drawer).getByRole('button', {\n    name: /submit/i,\n  });\n\n  await userEvent.click(submitButton);\n\n  await waitFor(() => expect(drawer).not.toBeInTheDocument());\n\n  expect(\n    await screen.findByRole('heading', { name: newTitle }),\n  ).toBeInTheDocument();\n  expect(await screen.findByText(newBody)).toBeInTheDocument();\n});\n\ntest(\n  'should create and delete a comment on the discussion',\n  async () => {\n    await renderDiscussion();\n\n    const comment = 'Hello World';\n\n    await userEvent.click(\n      screen.getByRole('button', { name: /create comment/i }),\n    );\n\n    const drawer = await screen.findByRole('dialog', {\n      name: /create comment/i,\n    });\n\n    const bodyField = await within(drawer).findByText(/body/i);\n\n    await userEvent.type(bodyField, comment);\n\n    const submitButton = await within(drawer).findByRole('button', {\n      name: /submit/i,\n    });\n\n    await userEvent.click(submitButton);\n\n    await waitFor(() => expect(drawer).not.toBeInTheDocument());\n\n    await screen.findByText(comment);\n\n    const commentsList = await screen.findByRole('list', {\n      name: 'comments',\n    });\n\n    const commentElements =\n      await within(commentsList).findAllByRole('listitem');\n\n    const commentElement = commentElements[0];\n\n    expect(commentElement).toBeInTheDocument();\n\n    const deleteCommentButton = within(commentElement).getByRole('button', {\n      name: /delete comment/i,\n      // exact: false,\n    });\n\n    await userEvent.click(deleteCommentButton);\n\n    const confirmationDialog = await screen.findByRole('dialog', {\n      name: /delete comment/i,\n    });\n\n    const confirmationDeleteButton = await within(\n      confirmationDialog,\n    ).findByRole('button', {\n      name: /delete/i,\n    });\n\n    await userEvent.click(confirmationDeleteButton);\n\n    await screen.findByText(/comment deleted/i);\n\n    await waitFor(() => {\n      expect(within(commentsList).queryByText(comment)).not.toBeInTheDocument();\n    });\n  },\n  {\n    timeout: 20000,\n  },\n);\n"
  },
  {
    "path": "apps/nextjs-pages/src/app/pages/app/discussions/__tests__/discussions.test.tsx",
    "content": "import type { Mock } from 'vitest';\n\nimport { createDiscussion } from '@/testing/data-generators';\nimport {\n  renderApp,\n  screen,\n  userEvent,\n  waitFor,\n  waitForLoadingToFinish,\n  within,\n} from '@/testing/test-utils';\nimport { formatDate } from '@/utils/format';\n\nimport { DiscussionsPage } from '../discussions';\n\nbeforeAll(() => {\n  vi.spyOn(console, 'error').mockImplementation(() => {});\n});\n\nafterAll(() => {\n  (console.error as Mock).mockRestore();\n});\n\ntest(\n  'should create, render and delete discussions',\n  { timeout: 10000 },\n  async () => {\n    await renderApp(<DiscussionsPage />);\n\n    await waitForLoadingToFinish();\n\n    const newDiscussion = createDiscussion();\n\n    expect(await screen.findByText(/no entries/i)).toBeInTheDocument();\n\n    await userEvent.click(\n      screen.getByRole('button', { name: /create discussion/i }),\n    );\n\n    const drawer = await screen.findByRole('dialog', {\n      name: /create discussion/i,\n    });\n\n    const titleField = within(drawer).getByText(/title/i);\n    const bodyField = within(drawer).getByText(/body/i);\n\n    await userEvent.type(titleField, newDiscussion.title);\n    await userEvent.type(bodyField, newDiscussion.body);\n\n    const submitButton = within(drawer).getByRole('button', {\n      name: /submit/i,\n    });\n\n    await userEvent.click(submitButton);\n\n    await waitFor(() => expect(drawer).not.toBeInTheDocument());\n\n    const row = await screen.findByRole(\n      'row',\n      {\n        name: `${newDiscussion.title} ${formatDate(newDiscussion.createdAt)} View Delete Discussion`,\n      },\n      { timeout: 5000 },\n    );\n\n    expect(\n      within(row).getByRole('cell', {\n        name: newDiscussion.title,\n      }),\n    ).toBeInTheDocument();\n\n    await userEvent.click(\n      within(row).getByRole('button', {\n        name: /delete discussion/i,\n      }),\n    );\n\n    const confirmationDialog = await screen.findByRole('dialog', {\n      name: /delete discussion/i,\n    });\n\n    const confirmationDeleteButton = within(confirmationDialog).getByRole(\n      'button',\n      {\n        name: /delete discussion/i,\n      },\n    );\n\n    await userEvent.click(confirmationDeleteButton);\n\n    await screen.findByText(/discussion deleted/i);\n\n    expect(\n      within(row).queryByRole('cell', {\n        name: newDiscussion.title,\n      }),\n    ).not.toBeInTheDocument();\n  },\n);\n"
  },
  {
    "path": "apps/nextjs-pages/src/app/pages/app/discussions/discussion.tsx",
    "content": "import {\n  dehydrate,\n  HydrationBoundary,\n  QueryClient,\n} from '@tanstack/react-query';\nimport { GetServerSideProps, InferGetServerSidePropsType } from 'next';\nimport { useRouter } from 'next/router';\nimport { ReactElement } from 'react';\nimport { ErrorBoundary } from 'react-error-boundary';\n\nimport { ContentLayout, DashboardLayout } from '@/components/layouts';\nimport { Spinner } from '@/components/ui/spinner';\nimport { getInfiniteCommentsQueryOptions } from '@/features/comments/api/get-comments';\nimport { Comments } from '@/features/comments/components/comments';\nimport {\n  useDiscussion,\n  getDiscussionQueryOptions,\n} from '@/features/discussions/api/get-discussion';\nimport { DiscussionView } from '@/features/discussions/components/discussion-view';\n\ntype DiscussionPageProps = {\n  dehydratedState?: unknown;\n};\n\nexport const getServerSideProps = (async ({ query, req }) => {\n  const queryClient = new QueryClient();\n  const discussionId = query.discussionId as string;\n  const cookie = req.headers.cookie;\n\n  await queryClient.prefetchQuery(\n    getDiscussionQueryOptions(discussionId, cookie),\n  );\n  await queryClient.prefetchInfiniteQuery(\n    getInfiniteCommentsQueryOptions(discussionId, cookie),\n  );\n\n  return {\n    props: {\n      dehydratedState: dehydrate(queryClient),\n    },\n  };\n}) satisfies GetServerSideProps<DiscussionPageProps>;\n\nexport const DiscussionPage = () => {\n  const router = useRouter();\n  const discussionId = router.query.discussionId as string;\n\n  const discussionQuery = useDiscussion({\n    discussionId,\n  });\n\n  if (discussionQuery.isLoading) {\n    return (\n      <div className=\"flex h-48 w-full items-center justify-center\">\n        <Spinner size=\"lg\" />\n      </div>\n    );\n  }\n\n  const discussion = discussionQuery.data?.data;\n\n  if (!discussion) return null;\n\n  return (\n    <ContentLayout title={discussion.title}>\n      <DiscussionView discussionId={discussionId} />\n      <div className=\"mt-8\">\n        <ErrorBoundary\n          fallback={\n            <div>Failed to load comments. Try to refresh the page.</div>\n          }\n        >\n          <Comments discussionId={discussionId} />\n        </ErrorBoundary>\n      </div>\n    </ContentLayout>\n  );\n};\n\nDiscussionPage.getLayout = (page: ReactElement) => {\n  return <DashboardLayout>{page}</DashboardLayout>;\n};\n\nexport const PublicDiscussionPage = ({\n  dehydratedState,\n}: InferGetServerSidePropsType<typeof getServerSideProps>) => {\n  return (\n    <HydrationBoundary state={dehydratedState}>\n      <DiscussionPage />\n    </HydrationBoundary>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/app/pages/app/discussions/discussions.tsx",
    "content": "import { useQueryClient } from '@tanstack/react-query';\nimport { ReactElement } from 'react';\n\nimport { ContentLayout, DashboardLayout } from '@/components/layouts';\nimport { getInfiniteCommentsQueryOptions } from '@/features/comments/api/get-comments';\nimport { CreateDiscussion } from '@/features/discussions/components/create-discussion';\nimport { DiscussionsList } from '@/features/discussions/components/discussions-list';\n\nexport const DiscussionsPage = () => {\n  const queryClient = useQueryClient();\n\n  return (\n    <>\n      <div className=\"flex justify-end\">\n        <CreateDiscussion />\n      </div>\n      <div className=\"mt-4\">\n        <DiscussionsList\n          onDiscussionPrefetch={(id) => {\n            // Prefetch the comments data when the user hovers over the link in the list\n            queryClient.prefetchInfiniteQuery(\n              getInfiniteCommentsQueryOptions(id),\n            );\n          }}\n        />\n      </div>\n    </>\n  );\n};\n\nDiscussionsPage.getLayout = (page: ReactElement) => {\n  return (\n    <DashboardLayout>\n      <ContentLayout title=\"Discussions\">{page}</ContentLayout>\n    </DashboardLayout>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/app/pages/app/profile.tsx",
    "content": "import { ReactElement } from 'react';\n\nimport { ContentLayout, DashboardLayout } from '@/components/layouts';\nimport { UpdateProfile } from '@/features/users/components/update-profile';\nimport { useUser } from '@/lib/auth';\n\ntype EntryProps = {\n  label: string;\n  value: string;\n};\nconst Entry = ({ label, value }: EntryProps) => (\n  <div className=\"py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5\">\n    <dt className=\"text-sm font-medium text-gray-500\">{label}</dt>\n    <dd className=\"mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0\">\n      {value}\n    </dd>\n  </div>\n);\n\nexport const ProfilePage = () => {\n  const user = useUser();\n\n  if (!user.data) return null;\n\n  return (\n    <div className=\"overflow-hidden bg-white shadow sm:rounded-lg\">\n      <div className=\"px-4 py-5 sm:px-6\">\n        <div className=\"flex justify-between\">\n          <h3 className=\"text-lg font-medium leading-6 text-gray-900\">\n            User Information\n          </h3>\n          <UpdateProfile />\n        </div>\n        <p className=\"mt-1 max-w-2xl text-sm text-gray-500\">\n          Personal details of the user.\n        </p>\n      </div>\n      <div className=\"border-t border-gray-200 px-4 py-5 sm:p-0\">\n        <dl className=\"sm:divide-y sm:divide-gray-200\">\n          <Entry label=\"First Name\" value={user.data.firstName} />\n          <Entry label=\"Last Name\" value={user.data.lastName} />\n          <Entry label=\"Email Address\" value={user.data.email} />\n          <Entry label=\"Role\" value={user.data.role} />\n          <Entry label=\"Bio\" value={user.data.bio} />\n        </dl>\n      </div>\n    </div>\n  );\n};\n\nProfilePage.getLayout = (page: ReactElement) => {\n  return (\n    <DashboardLayout>\n      <ContentLayout title=\"Profile\">{page}</ContentLayout>\n    </DashboardLayout>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/app/pages/app/users.tsx",
    "content": "import { ReactElement } from 'react';\n\nimport { ContentLayout, DashboardLayout } from '@/components/layouts';\nimport { UsersList } from '@/features/users/components/users-list';\nimport { Authorization, ROLES } from '@/lib/authorization';\n\nexport const UsersPage = () => {\n  return (\n    <ContentLayout title=\"Users\">\n      <Authorization\n        forbiddenFallback={<div>Only admin can view this.</div>}\n        allowedRoles={[ROLES.ADMIN]}\n      >\n        <UsersList />\n      </Authorization>\n    </ContentLayout>\n  );\n};\n\nUsersPage.getLayout = (page: ReactElement) => {\n  return (\n    <DashboardLayout>\n      <ContentLayout title=\"Users\">{page}</ContentLayout>\n    </DashboardLayout>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/app/pages/auth/login.tsx",
    "content": "import { useRouter } from 'next/router';\nimport { ReactElement } from 'react';\n\nimport { AuthLayout } from '@/components/layouts/auth-layout';\nimport { paths } from '@/config/paths';\nimport { LoginForm } from '@/features/auth/components/login-form';\n\nexport const LoginPage = () => {\n  const router = useRouter();\n  const { redirectTo } = router.query;\n\n  return (\n    <LoginForm\n      onSuccess={() =>\n        router.replace(\n          `${redirectTo ? `${redirectTo}` : paths.app.dashboard.getHref()}`,\n        )\n      }\n    />\n  );\n};\n\nLoginPage.getLayout = (page: ReactElement) => {\n  return <AuthLayout title=\"Log in to your account\">{page}</AuthLayout>;\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/app/pages/auth/register.tsx",
    "content": "import { useRouter } from 'next/router';\nimport { ReactElement, useState } from 'react';\n\nimport { AuthLayout } from '@/components/layouts/auth-layout';\nimport { paths } from '@/config/paths';\nimport { RegisterForm } from '@/features/auth/components/register-form';\nimport { useTeams } from '@/features/teams/api/get-teams';\n\nexport const RegisterPage = () => {\n  const router = useRouter();\n\n  const { redirectTo } = router.query;\n\n  const [chooseTeam, setChooseTeam] = useState(false);\n\n  const teamsQuery = useTeams({\n    queryConfig: {\n      enabled: chooseTeam,\n    },\n  });\n\n  return (\n    <RegisterForm\n      onSuccess={() =>\n        router.replace(\n          `${redirectTo ? `${redirectTo}` : paths.app.dashboard.getHref()}`,\n        )\n      }\n      chooseTeam={chooseTeam}\n      setChooseTeam={() => setChooseTeam(!chooseTeam)}\n      teams={teamsQuery.data?.data}\n    />\n  );\n};\n\nRegisterPage.getLayout = (page: ReactElement) => {\n  return <AuthLayout title=\"Register your account\">{page}</AuthLayout>;\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/app/provider.tsx",
    "content": "import { QueryClient, QueryClientProvider } from '@tanstack/react-query';\nimport { ReactQueryDevtools } from '@tanstack/react-query-devtools';\nimport * as React from 'react';\nimport { ErrorBoundary } from 'react-error-boundary';\n\nimport { MainErrorFallback } from '@/components/errors/main';\nimport { Notifications } from '@/components/ui/notifications';\nimport { Spinner } from '@/components/ui/spinner';\nimport { queryConfig } from '@/lib/react-query';\n\ntype AppProviderProps = {\n  children: React.ReactNode;\n};\n\nexport const AppProvider = ({ children }: AppProviderProps) => {\n  const [queryClient] = React.useState(\n    () =>\n      new QueryClient({\n        defaultOptions: queryConfig,\n      }),\n  );\n\n  return (\n    <React.Suspense\n      fallback={\n        <div className=\"flex h-screen w-screen items-center justify-center\">\n          <Spinner size=\"xl\" />\n        </div>\n      }\n    >\n      <ErrorBoundary FallbackComponent={MainErrorFallback}>\n        <QueryClientProvider client={queryClient}>\n          {process.env.DEV && <ReactQueryDevtools />}\n          <Notifications />\n          {children}\n        </QueryClientProvider>\n      </ErrorBoundary>\n    </React.Suspense>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/errors/main.tsx",
    "content": "import { Button } from '../ui/button';\n\nexport const MainErrorFallback = () => {\n  return (\n    <div\n      className=\"flex h-screen w-screen flex-col items-center justify-center text-red-500\"\n      role=\"alert\"\n    >\n      <h2 className=\"text-lg font-semibold\">Ooops, something went wrong :( </h2>\n      <Button\n        className=\"mt-4\"\n        onClick={() => window.location.assign(window.location.origin)}\n      >\n        Refresh\n      </Button>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/layouts/auth-layout.tsx",
    "content": "import { useRouter } from 'next/router';\nimport * as React from 'react';\nimport { useEffect } from 'react';\n\nimport { Head } from '@/components/seo';\nimport { Link } from '@/components/ui/link';\nimport { paths } from '@/config/paths';\nimport { useUser } from '@/lib/auth';\n\ntype LayoutProps = {\n  children: React.ReactNode;\n  title: string;\n};\n\nexport const AuthLayout = ({ children, title }: LayoutProps) => {\n  const user = useUser();\n\n  const router = useRouter();\n\n  useEffect(() => {\n    if (user.data) {\n      router.replace(paths.app.dashboard.getHref());\n    }\n  }, [user.data, router]);\n\n  return (\n    <>\n      <Head title={title} />\n      <div className=\"flex min-h-screen flex-col justify-center bg-gray-50 py-12 sm:px-6 lg:px-8\">\n        <div className=\"sm:mx-auto sm:w-full sm:max-w-md\">\n          <div className=\"flex justify-center\">\n            <Link\n              className=\"flex items-center text-white\"\n              href={paths.home.getHref()}\n            >\n              <img className=\"h-24 w-auto\" src=\"/logo.svg\" alt=\"Workflow\" />\n            </Link>\n          </div>\n\n          <h2 className=\"mt-3 text-center text-3xl font-extrabold text-gray-900\">\n            {title}\n          </h2>\n        </div>\n\n        <div className=\"mt-8 sm:mx-auto sm:w-full sm:max-w-md\">\n          <div className=\"bg-white px-4 py-8 shadow sm:rounded-lg sm:px-10\">\n            {children}\n          </div>\n        </div>\n      </div>\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/layouts/content-layout.tsx",
    "content": "import * as React from 'react';\n\nimport { Head } from '../seo';\n\ntype ContentLayoutProps = {\n  children: React.ReactNode;\n  title: string;\n};\n\nexport const ContentLayout = ({ children, title }: ContentLayoutProps) => {\n  return (\n    <>\n      <Head title={title} />\n      <div className=\"py-6\">\n        <div className=\"mx-auto max-w-7xl px-4 sm:px-6 md:px-8\">\n          <h1 className=\"text-2xl font-semibold text-gray-900\">{title}</h1>\n        </div>\n        <div className=\"mx-auto max-w-7xl px-4 py-6 sm:px-6 md:px-8\">\n          {children}\n        </div>\n      </div>\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/layouts/dashboard-layout.tsx",
    "content": "import { Home, PanelLeft, Folder, Users, User2 } from 'lucide-react';\nimport NextLink from 'next/link';\nimport { useRouter } from 'next/router';\nimport { useEffect, useState, Suspense } from 'react';\nimport { ErrorBoundary } from 'react-error-boundary';\n\nimport { Button } from '@/components/ui/button';\nimport { Drawer, DrawerContent, DrawerTrigger } from '@/components/ui/drawer';\nimport { Spinner } from '@/components/ui/spinner';\nimport { paths } from '@/config/paths';\nimport { AuthLoader, useLogout } from '@/lib/auth';\nimport { ROLES, useAuthorization } from '@/lib/authorization';\nimport { cn } from '@/utils/cn';\n\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from '../ui/dropdown';\nimport { Link } from '../ui/link';\n\ntype SideNavigationItem = {\n  name: string;\n  to: string;\n  icon: (props: React.SVGProps<SVGSVGElement>) => JSX.Element;\n};\n\nconst Logo = () => {\n  return (\n    <Link className=\"flex items-center text-white\" href={paths.home.getHref()}>\n      <img className=\"h-8 w-auto\" src=\"/logo.svg\" alt=\"Workflow\" />\n      <span className=\"text-sm font-semibold text-white\">\n        Bulletproof React\n      </span>\n    </Link>\n  );\n};\n\nconst Progress = () => {\n  const router = useRouter();\n  const [progress, setProgress] = useState(0);\n\n  useEffect(() => {\n    const handleRouteChangeStart = () => {\n      setProgress(0);\n      const timer = setInterval(() => {\n        setProgress((oldProgress) => {\n          if (oldProgress === 100) {\n            clearInterval(timer);\n            return 100;\n          }\n          const newProgress = oldProgress + 10;\n          return newProgress > 100 ? 100 : newProgress;\n        });\n      }, 300);\n\n      return () => {\n        clearInterval(timer);\n      };\n    };\n\n    const handleRouteChangeComplete = () => {\n      setProgress(100);\n      setTimeout(() => {\n        setProgress(0);\n      }, 500); // Adjust the delay as needed\n    };\n\n    router.events.on('routeChangeStart', handleRouteChangeStart);\n    router.events.on('routeChangeComplete', handleRouteChangeComplete);\n    router.events.on('routeChangeError', handleRouteChangeComplete);\n\n    return () => {\n      router.events.off('routeChangeStart', handleRouteChangeStart);\n      router.events.off('routeChangeComplete', handleRouteChangeComplete);\n      router.events.off('routeChangeError', handleRouteChangeComplete);\n    };\n  }, [router.events]);\n\n  if (progress === 0) {\n    return null;\n  }\n\n  return (\n    <div\n      className=\"fixed left-0 top-0 h-1 bg-blue-500 transition-all duration-200 ease-in-out\"\n      style={{ width: `${progress}%` }}\n    ></div>\n  );\n};\n\nconst Layout = ({ children }: { children: React.ReactNode }) => {\n  const logout = useLogout();\n  const { checkAccess } = useAuthorization();\n  const router = useRouter();\n  const navigation = [\n    { name: 'Dashboard', to: paths.app.dashboard.getHref(), icon: Home },\n    { name: 'Discussions', to: paths.app.discussions.getHref(), icon: Folder },\n    checkAccess({ allowedRoles: [ROLES.ADMIN] }) && {\n      name: 'Users',\n      to: paths.app.users.getHref(),\n      icon: Users,\n    },\n  ].filter(Boolean) as SideNavigationItem[];\n\n  return (\n    <div className=\"flex min-h-screen w-full flex-col bg-muted/40\">\n      <aside className=\"fixed inset-y-0 left-0 z-10 hidden w-60 flex-col border-r bg-black sm:flex\">\n        <nav className=\"flex flex-col items-center gap-4 px-2 py-4\">\n          <div className=\"flex h-16 shrink-0 items-center px-4\">\n            <Logo />\n          </div>\n          {navigation.map((item) => {\n            const isActive = router.pathname === item.to;\n            return (\n              <NextLink\n                key={item.name}\n                href={item.to}\n                className={cn(\n                  'text-gray-300 hover:bg-gray-700 hover:text-white',\n                  'group flex flex-1 w-full items-center rounded-md p-2 text-base font-medium',\n                  isActive && 'bg-gray-900 text-white',\n                )}\n              >\n                <item.icon\n                  className={cn(\n                    'text-gray-400 group-hover:text-gray-300',\n                    'mr-4 size-6 shrink-0',\n                  )}\n                  aria-hidden=\"true\"\n                />\n                {item.name}\n              </NextLink>\n            );\n          })}\n        </nav>\n      </aside>\n      <div className=\"flex flex-col sm:gap-4 sm:py-4 sm:pl-60\">\n        <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\">\n          <Progress />\n          <Drawer>\n            <DrawerTrigger asChild>\n              <Button size=\"icon\" variant=\"outline\" className=\"sm:hidden\">\n                <PanelLeft className=\"size-5\" />\n                <span className=\"sr-only\">Toggle Menu</span>\n              </Button>\n            </DrawerTrigger>\n            <DrawerContent\n              side=\"left\"\n              className=\"bg-black pt-10 text-white sm:max-w-60\"\n            >\n              <nav className=\"grid gap-6 text-lg font-medium\">\n                <div className=\"flex h-16 shrink-0 items-center px-4\">\n                  <Logo />\n                </div>\n                {navigation.map((item) => {\n                  const isActive = router.pathname === item.to;\n                  return (\n                    <NextLink\n                      key={item.name}\n                      href={item.to}\n                      className={cn(\n                        'text-gray-300 hover:bg-gray-700 hover:text-white',\n                        'group flex flex-1 w-full items-center rounded-md p-2 text-base font-medium',\n                        isActive && 'bg-gray-900 text-white',\n                      )}\n                    >\n                      <item.icon\n                        className={cn(\n                          'text-gray-400 group-hover:text-gray-300',\n                          'mr-4 size-6 shrink-0',\n                        )}\n                        aria-hidden=\"true\"\n                      />\n                      {item.name}\n                    </NextLink>\n                  );\n                })}\n              </nav>\n            </DrawerContent>\n          </Drawer>\n          <DropdownMenu>\n            <DropdownMenuTrigger asChild>\n              <Button\n                variant=\"outline\"\n                size=\"icon\"\n                className=\"overflow-hidden rounded-full\"\n              >\n                <span className=\"sr-only\">Open user menu</span>\n                <User2 className=\"size-6 rounded-full\" />\n              </Button>\n            </DropdownMenuTrigger>\n            <DropdownMenuContent align=\"end\">\n              <DropdownMenuItem\n                onClick={() => router.push(paths.app.profile.getHref())}\n                className={cn('block px-4 py-2 text-sm text-gray-700')}\n              >\n                Your Profile\n              </DropdownMenuItem>\n              <DropdownMenuSeparator />\n              <DropdownMenuItem\n                className={cn('block px-4 py-2 text-sm text-gray-700 w-full')}\n                onClick={() => logout.mutate({})}\n              >\n                Sign Out\n              </DropdownMenuItem>\n            </DropdownMenuContent>\n          </DropdownMenu>\n        </header>\n        <main className=\"grid flex-1 items-start gap-4 p-4 sm:px-6 sm:py-0 md:gap-8\">\n          {children}\n        </main>\n      </div>\n    </div>\n  );\n};\n\nexport const DashboardLayout = ({\n  children,\n}: {\n  children: React.ReactNode;\n}) => {\n  const router = useRouter();\n  return (\n    <Layout>\n      <Suspense\n        fallback={\n          <div className=\"flex size-full items-center justify-center\">\n            <Spinner size=\"xl\" />\n          </div>\n        }\n      >\n        <ErrorBoundary\n          key={router.pathname}\n          fallback={<div>Something went wrong!</div>}\n        >\n          <AuthLoader\n            renderLoading={() => (\n              <div className=\"flex size-full items-center justify-center\">\n                <Spinner size=\"xl\" />\n              </div>\n            )}\n          >\n            {children}\n          </AuthLoader>\n        </ErrorBoundary>\n      </Suspense>\n    </Layout>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/layouts/index.ts",
    "content": "export * from './content-layout';\nexport * from './dashboard-layout';\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/seo/head.tsx",
    "content": "import NextHead from 'next/head';\n\ntype HeadProps = {\n  title?: string;\n  description?: string;\n};\n\nexport const Head = ({ title = '', description = '' }: HeadProps = {}) => {\n  return (\n    <NextHead>\n      <title>{title}</title>\n      <meta name=\"description\" content={description} />\n    </NextHead>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/seo/index.ts",
    "content": "export * from './head';\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/button/button.stories.tsx",
    "content": "import { Meta, StoryObj } from '@storybook/react';\n\nimport { Button } from './button';\n\nconst meta: Meta<typeof Button> = {\n  component: Button,\n};\n\nexport default meta;\ntype Story = StoryObj<typeof Button>;\n\nexport const Default: Story = {\n  args: {\n    children: 'Button',\n    variant: 'default',\n  },\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/button/button.tsx",
    "content": "import { Slot } from '@radix-ui/react-slot';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport * as React from 'react';\n\nimport { cn } from '@/utils/cn';\n\nimport { Spinner } from '../spinner';\n\nconst buttonVariants = cva(\n  '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',\n  {\n    variants: {\n      variant: {\n        default:\n          'bg-primary text-primary-foreground shadow hover:bg-primary/90',\n        destructive:\n          'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',\n        outline:\n          'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',\n        secondary:\n          'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',\n        ghost: 'hover:bg-accent hover:text-accent-foreground',\n        link: 'text-primary underline-offset-4 hover:underline',\n      },\n      size: {\n        default: 'h-9 px-4 py-2',\n        sm: 'h-8 rounded-md px-3 text-xs',\n        lg: 'h-10 rounded-md px-8',\n        icon: 'size-9',\n      },\n    },\n    defaultVariants: {\n      variant: 'default',\n      size: 'default',\n    },\n  },\n);\n\nexport type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> &\n  VariantProps<typeof buttonVariants> & {\n    asChild?: boolean;\n    isLoading?: boolean;\n    icon?: React.ReactNode;\n  };\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n  (\n    {\n      className,\n      variant,\n      size,\n      asChild = false,\n      children,\n      isLoading,\n      icon,\n      ...props\n    },\n    ref,\n  ) => {\n    const Comp = asChild ? Slot : 'button';\n    return (\n      <Comp\n        className={cn(buttonVariants({ variant, size, className }))}\n        ref={ref}\n        {...props}\n      >\n        {isLoading && <Spinner size=\"sm\" className=\"text-current\" />}\n        {!isLoading && icon && <span className=\"mr-2\">{icon}</span>}\n        <span className=\"mx-2\">{children}</span>\n      </Comp>\n    );\n  },\n);\nButton.displayName = 'Button';\n\nexport { Button, buttonVariants };\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/button/index.ts",
    "content": "export * from './button';\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/dialog/__tests__/dialog.test.tsx",
    "content": "import * as React from 'react';\n\nimport { Button } from '@/components/ui/button';\nimport { useDisclosure } from '@/hooks/use-disclosure';\nimport { rtlRender, screen, userEvent, waitFor } from '@/testing/test-utils';\n\nimport {\n  Dialog,\n  DialogContent,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from '../dialog';\n\nconst openButtonText = 'Open Modal';\nconst cancelButtonText = 'Cancel';\nconst titleText = 'Modal Title';\n\nconst TestDialog = () => {\n  const { close, open, isOpen } = useDisclosure();\n  const cancelButtonRef = React.useRef(null);\n\n  return (\n    <Dialog\n      open={isOpen}\n      onOpenChange={(isOpen) => {\n        if (!isOpen) {\n          close();\n        } else {\n          open();\n        }\n      }}\n    >\n      <DialogTrigger asChild>\n        <Button variant=\"outline\">{openButtonText}</Button>\n      </DialogTrigger>\n      <DialogContent className=\"sm:max-w-[425px]\">\n        <DialogHeader>\n          <DialogTitle>{titleText}</DialogTitle>\n        </DialogHeader>\n\n        <DialogFooter>\n          <Button type=\"submit\">Submit</Button>\n          <Button ref={cancelButtonRef} variant=\"outline\" onClick={close}>\n            {cancelButtonText}\n          </Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n};\n\ntest('should handle basic dialog flow', async () => {\n  rtlRender(<TestDialog />);\n\n  expect(screen.queryByText(titleText)).not.toBeInTheDocument();\n\n  await userEvent.click(screen.getByRole('button', { name: openButtonText }));\n\n  expect(await screen.findByText(titleText)).toBeInTheDocument();\n\n  await userEvent.click(screen.getByRole('button', { name: cancelButtonText }));\n\n  await waitFor(() =>\n    expect(screen.queryByText(titleText)).not.toBeInTheDocument(),\n  );\n});\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/dialog/confirmation-dialog/__tests__/confirmation-dialog.test.tsx",
    "content": "import { Button } from '@/components/ui/button';\nimport { rtlRender, screen, userEvent, waitFor } from '@/testing/test-utils';\n\nimport { ConfirmationDialog } from '../confirmation-dialog';\n\ntest('should handle confirmation flow', async () => {\n  const titleText = 'Are you sure?';\n  const bodyText = 'Are you sure you want to delete this item?';\n  const confirmationButtonText = 'Confirm';\n  const openButtonText = 'Open';\n\n  await rtlRender(\n    <ConfirmationDialog\n      icon=\"danger\"\n      title={titleText}\n      body={bodyText}\n      confirmButton={<Button>{confirmationButtonText}</Button>}\n      triggerButton={<Button>{openButtonText}</Button>}\n    />,\n  );\n\n  expect(screen.queryByText(titleText)).not.toBeInTheDocument();\n\n  await userEvent.click(screen.getByRole('button', { name: openButtonText }));\n\n  expect(await screen.findByText(titleText)).toBeInTheDocument();\n\n  expect(screen.getByText(bodyText)).toBeInTheDocument();\n\n  await userEvent.click(screen.getByRole('button', { name: 'Cancel' }));\n\n  await waitFor(() =>\n    expect(screen.queryByText(titleText)).not.toBeInTheDocument(),\n  );\n\n  expect(screen.queryByText(bodyText)).not.toBeInTheDocument();\n});\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/dialog/confirmation-dialog/confirmation-dialog.stories.tsx",
    "content": "import { Meta, StoryObj } from '@storybook/react';\n\nimport { Button } from '@/components/ui/button';\n\nimport { ConfirmationDialog } from './confirmation-dialog';\n\nconst meta: Meta<typeof ConfirmationDialog> = {\n  component: ConfirmationDialog,\n};\n\nexport default meta;\n\ntype Story = StoryObj<typeof ConfirmationDialog>;\n\nexport const Danger: Story = {\n  args: {\n    icon: 'danger',\n    title: 'Confirmation',\n    body: 'Hello World',\n    confirmButton: <Button className=\"bg-red-500\">Confirm</Button>,\n    triggerButton: <Button>Open</Button>,\n  },\n};\n\nexport const Info: Story = {\n  args: {\n    icon: 'info',\n    title: 'Confirmation',\n    body: 'Hello World',\n    confirmButton: <Button>Confirm</Button>,\n    triggerButton: <Button>Open</Button>,\n  },\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/dialog/confirmation-dialog/confirmation-dialog.tsx",
    "content": "import { CircleAlert, Info } from 'lucide-react';\nimport * as React from 'react';\nimport { useEffect } from 'react';\n\nimport { Button } from '@/components/ui/button';\nimport { useDisclosure } from '@/hooks/use-disclosure';\n\nimport {\n  Dialog,\n  DialogContent,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from '../dialog';\n\nexport type ConfirmationDialogProps = {\n  triggerButton: React.ReactElement;\n  confirmButton: React.ReactElement;\n  title: string;\n  body?: string;\n  cancelButtonText?: string;\n  icon?: 'danger' | 'info';\n  isDone?: boolean;\n};\n\nexport const ConfirmationDialog = ({\n  triggerButton,\n  confirmButton,\n  title,\n  body = '',\n  cancelButtonText = 'Cancel',\n  icon = 'danger',\n  isDone = false,\n}: ConfirmationDialogProps) => {\n  const { close, open, isOpen } = useDisclosure();\n  const cancelButtonRef = React.useRef(null);\n\n  useEffect(() => {\n    if (isDone) {\n      close();\n    }\n  }, [isDone, close]);\n\n  return (\n    <Dialog\n      open={isOpen}\n      onOpenChange={(isOpen) => {\n        if (!isOpen) {\n          close();\n        } else {\n          open();\n        }\n      }}\n    >\n      <DialogTrigger asChild>{triggerButton}</DialogTrigger>\n      <DialogContent className=\"sm:max-w-[425px]\">\n        <DialogHeader className=\"flex\">\n          <DialogTitle className=\"flex items-center gap-2\">\n            {' '}\n            {icon === 'danger' && (\n              <CircleAlert className=\"size-6 text-red-600\" aria-hidden=\"true\" />\n            )}\n            {icon === 'info' && (\n              <Info className=\"size-6 text-blue-600\" aria-hidden=\"true\" />\n            )}\n            {title}\n          </DialogTitle>\n        </DialogHeader>\n\n        <div className=\"mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left\">\n          {body && (\n            <div className=\"mt-2\">\n              <p>{body}</p>\n            </div>\n          )}\n        </div>\n\n        <DialogFooter>\n          {confirmButton}\n          <Button ref={cancelButtonRef} variant=\"outline\" onClick={close}>\n            {cancelButtonText}\n          </Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/dialog/confirmation-dialog/index.ts",
    "content": "export * from './confirmation-dialog';\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/dialog/dialog.stories.tsx",
    "content": "import { Meta, StoryObj } from '@storybook/react';\nimport * as React from 'react';\n\nimport { Button } from '@/components/ui/button';\nimport { useDisclosure } from '@/hooks/use-disclosure';\n\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from './dialog';\n\nconst DemoDialog = () => {\n  const { close, open, isOpen } = useDisclosure();\n  const cancelButtonRef = React.useRef(null);\n\n  return (\n    <Dialog\n      open={isOpen}\n      onOpenChange={(isOpen) => {\n        if (!isOpen) {\n          close();\n        } else {\n          open();\n        }\n      }}\n    >\n      <DialogTrigger asChild>\n        <Button variant=\"outline\">Open Dialog</Button>\n      </DialogTrigger>\n      <DialogContent className=\"sm:max-w-[425px]\">\n        <DialogHeader>\n          <DialogTitle>Edit profile</DialogTitle>\n          <DialogDescription>Lorem ipsum</DialogDescription>\n        </DialogHeader>\n        <div className=\"grid gap-4 py-4\">Lorem ipsum</div>\n\n        <DialogFooter>\n          <Button type=\"submit\">Save changes</Button>\n          <Button ref={cancelButtonRef} variant=\"outline\" onClick={close}>\n            Cancel\n          </Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n};\n\nconst meta: Meta = {\n  component: Dialog,\n};\n\nexport default meta;\n\ntype Story = StoryObj<typeof Dialog>;\n\nexport const Demo: Story = {\n  render: () => <DemoDialog />,\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/dialog/dialog.tsx",
    "content": "import * as DialogPrimitive from '@radix-ui/react-dialog';\nimport { Cross2Icon } from '@radix-ui/react-icons';\nimport * as React from 'react';\n\nimport { cn } from '@/utils/cn';\n\nconst Dialog = DialogPrimitive.Root;\n\nconst DialogTrigger = DialogPrimitive.Trigger;\n\nconst DialogPortal = DialogPrimitive.Portal;\n\nconst DialogClose = DialogPrimitive.Close;\n\nconst DialogOverlay = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Overlay\n    ref={ref}\n    className={cn(\n      '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',\n      className,\n    )}\n    {...props}\n  />\n));\nDialogOverlay.displayName = DialogPrimitive.Overlay.displayName;\n\nconst DialogContent = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n  <DialogPortal>\n    <DialogOverlay />\n    <DialogPrimitive.Content\n      ref={ref}\n      className={cn(\n        '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',\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      <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\">\n        <Cross2Icon className=\"size-4\" />\n        <span className=\"sr-only\">Close</span>\n      </DialogPrimitive.Close>\n    </DialogPrimitive.Content>\n  </DialogPortal>\n));\nDialogContent.displayName = DialogPrimitive.Content.displayName;\n\nconst DialogHeader = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      'flex flex-col space-y-1.5 text-center sm:text-left',\n      className,\n    )}\n    {...props}\n  />\n);\nDialogHeader.displayName = 'DialogHeader';\n\nconst DialogFooter = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',\n      className,\n    )}\n    {...props}\n  />\n);\nDialogFooter.displayName = 'DialogFooter';\n\nconst DialogTitle = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Title\n    ref={ref}\n    className={cn(\n      'text-lg font-semibold leading-none tracking-tight',\n      className,\n    )}\n    {...props}\n  />\n));\nDialogTitle.displayName = DialogPrimitive.Title.displayName;\n\nconst DialogDescription = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Description\n    ref={ref}\n    className={cn('text-sm text-muted-foreground', className)}\n    {...props}\n  />\n));\nDialogDescription.displayName = DialogPrimitive.Description.displayName;\n\nexport {\n  Dialog,\n  DialogPortal,\n  DialogOverlay,\n  DialogTrigger,\n  DialogClose,\n  DialogContent,\n  DialogHeader,\n  DialogFooter,\n  DialogTitle,\n  DialogDescription,\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/dialog/index.ts",
    "content": "export * from './dialog';\nexport * from './confirmation-dialog';\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/drawer/__tests__/drawer.test.tsx",
    "content": "import { Button } from '@/components/ui/button';\nimport { rtlRender, screen, userEvent, waitFor } from '@/testing/test-utils';\n\nimport {\n  Drawer,\n  DrawerClose,\n  DrawerContent,\n  DrawerFooter,\n  DrawerHeader,\n  DrawerTitle,\n  DrawerTrigger,\n} from '../drawer';\n\nconst openButtonText = 'Open Drawer';\nconst titleText = 'Drawer Title';\nconst cancelButtonText = 'Cancel';\nconst drawerContentText = 'Hello From Drawer';\n\nconst TestDrawer = () => {\n  return (\n    <Drawer>\n      <DrawerTrigger asChild>\n        <Button variant=\"outline\">{openButtonText}</Button>\n      </DrawerTrigger>\n      <DrawerContent className=\"flex max-w-[800px] flex-col justify-between sm:max-w-[540px]\">\n        <div className=\"flex flex-col\">\n          <DrawerHeader>\n            <DrawerTitle>{titleText}</DrawerTitle>\n          </DrawerHeader>\n          <div>{drawerContentText}</div>\n        </div>\n        <DrawerFooter>\n          <DrawerClose asChild>\n            <Button value=\"outline\" type=\"submit\">\n              {cancelButtonText}\n            </Button>\n          </DrawerClose>\n        </DrawerFooter>\n      </DrawerContent>\n    </Drawer>\n  );\n};\n\ntest('should handle basic drawer flow', async () => {\n  await rtlRender(<TestDrawer />);\n\n  expect(screen.queryByText(titleText)).not.toBeInTheDocument();\n\n  await userEvent.click(\n    screen.getByRole('button', {\n      name: openButtonText,\n    }),\n  );\n\n  expect(await screen.findByText(titleText)).toBeInTheDocument();\n\n  await userEvent.click(\n    screen.getByRole('button', {\n      name: cancelButtonText,\n    }),\n  );\n\n  await waitFor(() =>\n    expect(screen.queryByText(titleText)).not.toBeInTheDocument(),\n  );\n});\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/drawer/drawer.stories.tsx",
    "content": "import { Meta, StoryObj } from '@storybook/react';\n\nimport { Button } from '@/components/ui/button';\nimport { useDisclosure } from '@/hooks/use-disclosure';\n\nimport {\n  Drawer,\n  DrawerClose,\n  DrawerContent,\n  DrawerDescription,\n  DrawerFooter,\n  DrawerHeader,\n  DrawerTitle,\n  DrawerTrigger,\n} from './drawer';\n\nconst meta: Meta<typeof Drawer> = {\n  component: Drawer,\n};\n\nexport default meta;\n\ntype Story = StoryObj<typeof Drawer>;\n\nconst DemoDrawer = () => {\n  const { close, open, isOpen } = useDisclosure();\n\n  return (\n    <Drawer\n      open={isOpen}\n      onOpenChange={(isOpen) => {\n        if (!isOpen) {\n          close();\n        } else {\n          open();\n        }\n      }}\n    >\n      <DrawerTrigger asChild>\n        <Button variant=\"outline\">Open</Button>\n      </DrawerTrigger>\n      <DrawerContent className=\"flex max-w-[800px] flex-col justify-between sm:max-w-[540px]\">\n        <div className=\"flex flex-col\">\n          <DrawerHeader>\n            <DrawerTitle>Drawer Header</DrawerTitle>\n            <DrawerDescription>\n              Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n            </DrawerDescription>\n          </DrawerHeader>\n          <div>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</div>\n        </div>\n        <DrawerFooter>\n          <DrawerClose asChild>\n            <Button type=\"submit\">Save changes</Button>\n          </DrawerClose>\n        </DrawerFooter>\n      </DrawerContent>\n    </Drawer>\n  );\n};\n\nexport const Default: Story = {\n  render: () => <DemoDrawer />,\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/drawer/drawer.tsx",
    "content": "import * as DrawerPrimitive from '@radix-ui/react-dialog';\nimport { Cross2Icon } from '@radix-ui/react-icons';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport * as React from 'react';\n\nimport { cn } from '@/utils/cn';\n\nconst Drawer = DrawerPrimitive.Root;\n\nconst DrawerTrigger = DrawerPrimitive.Trigger;\n\nconst DrawerClose = DrawerPrimitive.Close;\n\nconst DrawerPortal = DrawerPrimitive.Portal;\n\nconst DrawerOverlay = React.forwardRef<\n  React.ElementRef<typeof DrawerPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <DrawerPrimitive.Overlay\n    className={cn(\n      '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',\n      className,\n    )}\n    {...props}\n    ref={ref}\n  />\n));\nDrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;\n\nconst drawerVariants = cva(\n  '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',\n  {\n    variants: {\n      side: {\n        top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',\n        bottom:\n          'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',\n        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',\n        right:\n          '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',\n      },\n    },\n    defaultVariants: {\n      side: 'right',\n    },\n  },\n);\n\ntype DrawerContentProps = React.ComponentPropsWithoutRef<\n  typeof DrawerPrimitive.Content\n> &\n  VariantProps<typeof drawerVariants>;\n\nconst DrawerContent = React.forwardRef<\n  React.ElementRef<typeof DrawerPrimitive.Content>,\n  DrawerContentProps\n>(({ side = 'right', className, children, ...props }, ref) => (\n  <DrawerPortal>\n    <DrawerOverlay />\n    <DrawerPrimitive.Content\n      ref={ref}\n      className={cn(drawerVariants({ side }), className)}\n      {...props}\n    >\n      {children}\n      <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\">\n        <Cross2Icon className=\"size-4\" />\n        <span className=\"sr-only\">Close</span>\n      </DrawerPrimitive.Close>\n    </DrawerPrimitive.Content>\n  </DrawerPortal>\n));\nDrawerContent.displayName = DrawerPrimitive.Content.displayName;\n\nconst DrawerHeader = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      'flex flex-col space-y-2 text-center sm:text-left',\n      className,\n    )}\n    {...props}\n  />\n);\nDrawerHeader.displayName = 'DrawerHeader';\n\nconst DrawerFooter = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',\n      className,\n    )}\n    {...props}\n  />\n);\nDrawerFooter.displayName = 'DrawerFooter';\n\nconst DrawerTitle = React.forwardRef<\n  React.ElementRef<typeof DrawerPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <DrawerPrimitive.Title\n    ref={ref}\n    className={cn('text-lg font-semibold text-foreground', className)}\n    {...props}\n  />\n));\nDrawerTitle.displayName = DrawerPrimitive.Title.displayName;\n\nconst DrawerDescription = React.forwardRef<\n  React.ElementRef<typeof DrawerPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <DrawerPrimitive.Description\n    ref={ref}\n    className={cn('text-sm text-muted-foreground', className)}\n    {...props}\n  />\n));\nDrawerDescription.displayName = DrawerPrimitive.Description.displayName;\n\nexport {\n  Drawer,\n  DrawerPortal,\n  DrawerOverlay,\n  DrawerTrigger,\n  DrawerClose,\n  DrawerContent,\n  DrawerHeader,\n  DrawerFooter,\n  DrawerTitle,\n  DrawerDescription,\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/drawer/index.ts",
    "content": "export * from './drawer';\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/dropdown/dropdown.stories.tsx",
    "content": "import type { Meta } from '@storybook/react';\nimport React from 'react';\n\nimport { Button } from '@/components/ui/button';\n\nimport {\n  DropdownMenu,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuCheckboxItem,\n  DropdownMenuRadioItem,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuSub,\n  DropdownMenuSubTrigger,\n  DropdownMenuSubContent,\n  DropdownMenuRadioGroup,\n} from './dropdown';\n\nconst meta: Meta = {\n  component: DropdownMenu,\n};\n\nexport default meta;\n\nexport const Default = () => (\n  <DropdownMenu>\n    <DropdownMenuTrigger asChild>\n      <Button>Open Menu</Button>\n    </DropdownMenuTrigger>\n    <DropdownMenuContent>\n      <DropdownMenuItem>Item One</DropdownMenuItem>\n      <DropdownMenuItem>Item Two</DropdownMenuItem>\n      <DropdownMenuSeparator />\n      <DropdownMenuItem>Item Three</DropdownMenuItem>\n    </DropdownMenuContent>\n  </DropdownMenu>\n);\n\nexport const WithCheckboxItems = () => {\n  const [checked, setChecked] = React.useState(true);\n  const [checked2, setChecked2] = React.useState(false);\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <Button>Open Menu</Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent>\n        <DropdownMenuCheckboxItem\n          checked={checked}\n          onCheckedChange={setChecked}\n        >\n          Option One\n        </DropdownMenuCheckboxItem>\n        <DropdownMenuCheckboxItem\n          checked={checked2}\n          onCheckedChange={setChecked2}\n        >\n          Option Two\n        </DropdownMenuCheckboxItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n};\n\nexport const WithRadioItems = () => {\n  const [value, setValue] = React.useState('one');\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <Button>Open Menu</Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent>\n        <DropdownMenuLabel>Select an option</DropdownMenuLabel>\n        <DropdownMenuSeparator />\n        <DropdownMenuRadioGroup value={value} onValueChange={setValue}>\n          <DropdownMenuRadioItem value=\"one\">Option One</DropdownMenuRadioItem>\n          <DropdownMenuRadioItem value=\"two\">Option Two</DropdownMenuRadioItem>\n          <DropdownMenuRadioItem value=\"three\">\n            Option Three\n          </DropdownMenuRadioItem>\n        </DropdownMenuRadioGroup>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n};\n\nexport const WithSubmenus = () => (\n  <DropdownMenu>\n    <DropdownMenuTrigger>\n      <Button>Open Menu</Button>\n    </DropdownMenuTrigger>\n    <DropdownMenuContent>\n      <DropdownMenuItem>Item One</DropdownMenuItem>\n      <DropdownMenuSub>\n        <DropdownMenuSubTrigger>More Options</DropdownMenuSubTrigger>\n        <DropdownMenuSubContent>\n          <DropdownMenuItem>Sub Item One</DropdownMenuItem>\n          <DropdownMenuItem>Sub Item Two</DropdownMenuItem>\n        </DropdownMenuSubContent>\n      </DropdownMenuSub>\n      <DropdownMenuItem>Item Three</DropdownMenuItem>\n    </DropdownMenuContent>\n  </DropdownMenu>\n);\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/dropdown/dropdown.tsx",
    "content": "import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';\nimport {\n  CheckIcon,\n  ChevronRightIcon,\n  DotFilledIcon,\n} from '@radix-ui/react-icons';\nimport * as React from 'react';\n\nimport { cn } from '@/utils/cn';\n\nconst DropdownMenu = DropdownMenuPrimitive.Root;\n\nconst DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;\n\nconst DropdownMenuGroup = DropdownMenuPrimitive.Group;\n\nconst DropdownMenuPortal = DropdownMenuPrimitive.Portal;\n\nconst DropdownMenuSub = DropdownMenuPrimitive.Sub;\n\nconst DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;\n\nconst DropdownMenuSubTrigger = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {\n    inset?: boolean;\n  }\n>(({ className, inset, children, ...props }, ref) => (\n  <DropdownMenuPrimitive.SubTrigger\n    ref={ref}\n    className={cn(\n      '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',\n      inset && 'pl-8',\n      className,\n    )}\n    {...props}\n  >\n    {children}\n    <ChevronRightIcon className=\"ml-auto size-4\" />\n  </DropdownMenuPrimitive.SubTrigger>\n));\nDropdownMenuSubTrigger.displayName =\n  DropdownMenuPrimitive.SubTrigger.displayName;\n\nconst DropdownMenuSubContent = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>\n>(({ className, ...props }, ref) => (\n  <DropdownMenuPrimitive.SubContent\n    ref={ref}\n    className={cn(\n      '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',\n      className,\n    )}\n    {...props}\n  />\n));\nDropdownMenuSubContent.displayName =\n  DropdownMenuPrimitive.SubContent.displayName;\n\nconst DropdownMenuContent = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>\n>(({ className, sideOffset = 4, ...props }, ref) => (\n  <DropdownMenuPrimitive.Portal>\n    <DropdownMenuPrimitive.Content\n      ref={ref}\n      sideOffset={sideOffset}\n      className={cn(\n        'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md',\n        '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',\n        className,\n      )}\n      {...props}\n    />\n  </DropdownMenuPrimitive.Portal>\n));\nDropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;\n\nconst DropdownMenuItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {\n    inset?: boolean;\n  }\n>(({ className, inset, ...props }, ref) => (\n  <DropdownMenuPrimitive.Item\n    ref={ref}\n    className={cn(\n      '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',\n      inset && 'pl-8',\n      className,\n    )}\n    {...props}\n  />\n));\nDropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;\n\nconst DropdownMenuCheckboxItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>\n>(({ className, children, checked, ...props }, ref) => (\n  <DropdownMenuPrimitive.CheckboxItem\n    ref={ref}\n    className={cn(\n      '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',\n      className,\n    )}\n    checked={checked}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex size-3.5 items-center justify-center\">\n      <DropdownMenuPrimitive.ItemIndicator>\n        <CheckIcon className=\"size-4\" />\n      </DropdownMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </DropdownMenuPrimitive.CheckboxItem>\n));\nDropdownMenuCheckboxItem.displayName =\n  DropdownMenuPrimitive.CheckboxItem.displayName;\n\nconst DropdownMenuRadioItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>\n>(({ className, children, ...props }, ref) => (\n  <DropdownMenuPrimitive.RadioItem\n    ref={ref}\n    className={cn(\n      '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',\n      className,\n    )}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex size-3.5 items-center justify-center\">\n      <DropdownMenuPrimitive.ItemIndicator>\n        <DotFilledIcon className=\"size-4 fill-current\" />\n      </DropdownMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </DropdownMenuPrimitive.RadioItem>\n));\nDropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;\n\nconst DropdownMenuLabel = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {\n    inset?: boolean;\n  }\n>(({ className, inset, ...props }, ref) => (\n  <DropdownMenuPrimitive.Label\n    ref={ref}\n    className={cn(\n      'px-2 py-1.5 text-sm font-semibold',\n      inset && 'pl-8',\n      className,\n    )}\n    {...props}\n  />\n));\nDropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;\n\nconst DropdownMenuSeparator = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <DropdownMenuPrimitive.Separator\n    ref={ref}\n    className={cn('-mx-1 my-1 h-px bg-muted', className)}\n    {...props}\n  />\n));\nDropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;\n\nconst DropdownMenuShortcut = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLSpanElement>) => {\n  return (\n    <span\n      className={cn('ml-auto text-xs tracking-widest opacity-60', className)}\n      {...props}\n    />\n  );\n};\nDropdownMenuShortcut.displayName = 'DropdownMenuShortcut';\n\nexport {\n  DropdownMenu,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuCheckboxItem,\n  DropdownMenuRadioItem,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuShortcut,\n  DropdownMenuGroup,\n  DropdownMenuPortal,\n  DropdownMenuSub,\n  DropdownMenuSubContent,\n  DropdownMenuSubTrigger,\n  DropdownMenuRadioGroup,\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/dropdown/index.ts",
    "content": "export * from './dropdown';\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/form/__tests__/form.test.tsx",
    "content": "import { SubmitHandler } from 'react-hook-form';\nimport { z } from 'zod';\n\nimport { Button } from '@/components/ui/button';\nimport { rtlRender, screen, waitFor, userEvent } from '@/testing/test-utils';\n\nimport { Form } from '../form';\nimport { Input } from '../input';\n\nconst testData = {\n  title: 'Hello World',\n};\n\nconst schema = z.object({\n  title: z.string().min(1, 'Required'),\n});\n\ntest('should render and submit a basic Form component', async () => {\n  const handleSubmit = vi.fn() as SubmitHandler<z.infer<typeof schema>>;\n\n  rtlRender(\n    <Form onSubmit={handleSubmit} schema={schema} id=\"my-form\">\n      {({ register, formState }) => (\n        <>\n          <Input\n            label=\"Title\"\n            error={formState.errors['title']}\n            registration={register('title')}\n          />\n\n          <Button name=\"submit\" type=\"submit\" className=\"w-full\">\n            Submit\n          </Button>\n        </>\n      )}\n    </Form>,\n  );\n\n  await userEvent.type(screen.getByLabelText(/title/i), testData.title);\n\n  await userEvent.click(screen.getByRole('button', { name: /submit/i }));\n\n  await waitFor(() =>\n    expect(handleSubmit).toHaveBeenCalledWith(testData, expect.anything()),\n  );\n});\n\ntest('should fail submission if validation fails', async () => {\n  const handleSubmit = vi.fn() as SubmitHandler<z.infer<typeof schema>>;\n\n  rtlRender(\n    <Form onSubmit={handleSubmit} schema={schema} id=\"my-form\">\n      {({ register, formState }) => (\n        <>\n          <Input\n            label=\"Title\"\n            error={formState.errors['title']}\n            registration={register('title')}\n          />\n\n          <Button name=\"submit\" type=\"submit\" className=\"w-full\">\n            Submit\n          </Button>\n        </>\n      )}\n    </Form>,\n  );\n\n  await userEvent.click(screen.getByRole('button', { name: /submit/i }));\n\n  await screen.findByRole('alert', { name: /required/i });\n\n  expect(handleSubmit).toHaveBeenCalledTimes(0);\n});\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/form/error.tsx",
    "content": "export type ErrorProps = {\n  errorMessage?: string | null;\n};\n\nexport const Error = ({ errorMessage }: ErrorProps) => {\n  if (!errorMessage) return null;\n\n  return (\n    <div\n      role=\"alert\"\n      aria-label={errorMessage}\n      className=\"text-sm font-semibold text-red-500\"\n    >\n      {errorMessage}\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/form/field-wrapper.tsx",
    "content": "import * as React from 'react';\nimport { type FieldError } from 'react-hook-form';\n\nimport { Error } from './error';\nimport { Label } from './label';\n\ntype FieldWrapperProps = {\n  label?: string;\n  className?: string;\n  children: React.ReactNode;\n  error?: FieldError | undefined;\n};\n\nexport type FieldWrapperPassThroughProps = Omit<\n  FieldWrapperProps,\n  'className' | 'children'\n>;\n\nexport const FieldWrapper = (props: FieldWrapperProps) => {\n  const { label, error, children } = props;\n  return (\n    <div>\n      <Label>\n        {label}\n        <div className=\"mt-1\">{children}</div>\n      </Label>\n      <Error errorMessage={error?.message} />\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/form/form-drawer.tsx",
    "content": "import * as React from 'react';\n\nimport { useDisclosure } from '@/hooks/use-disclosure';\n\nimport { Button } from '../button';\nimport {\n  Drawer,\n  DrawerClose,\n  DrawerContent,\n  DrawerFooter,\n  DrawerHeader,\n  DrawerTrigger,\n  DrawerTitle,\n} from '../drawer';\n\ntype FormDrawerProps = {\n  isDone: boolean;\n  triggerButton: React.ReactElement;\n  submitButton: React.ReactElement;\n  title: string;\n  children: React.ReactNode;\n};\n\nexport const FormDrawer = ({\n  title,\n  children,\n  isDone,\n  triggerButton,\n  submitButton,\n}: FormDrawerProps) => {\n  const { close, open, isOpen } = useDisclosure();\n\n  React.useEffect(() => {\n    if (isDone) {\n      close();\n    }\n  }, [isDone, close]);\n\n  return (\n    <Drawer\n      open={isOpen}\n      onOpenChange={(isOpen) => {\n        if (!isOpen) {\n          close();\n        } else {\n          open();\n        }\n      }}\n    >\n      <DrawerTrigger asChild>{triggerButton}</DrawerTrigger>\n      <DrawerContent className=\"flex max-w-[800px] flex-col justify-between sm:max-w-[540px]\">\n        <div className=\"flex flex-col\">\n          <DrawerHeader>\n            <DrawerTitle>{title}</DrawerTitle>\n          </DrawerHeader>\n          <div>{children}</div>\n        </div>\n        <DrawerFooter>\n          <DrawerClose asChild>\n            <Button variant=\"outline\" type=\"submit\">\n              Close\n            </Button>\n          </DrawerClose>\n          {submitButton}\n        </DrawerFooter>\n      </DrawerContent>\n    </Drawer>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/form/form.stories.tsx",
    "content": "import { Meta, StoryObj } from '@storybook/react';\nimport { z } from 'zod';\n\nimport { Button } from '../button';\n\nimport { Form } from './form';\nimport { FormDrawer } from './form-drawer';\nimport { Input } from './input';\nimport { Select } from './select';\nimport { Textarea } from './textarea';\n\nconst MyForm = ({ hideSubmit = false }: { hideSubmit?: boolean }) => {\n  return (\n    <Form\n      onSubmit={async (values) => {\n        alert(JSON.stringify(values, null, 2));\n      }}\n      schema={z.object({\n        title: z.string().min(1, 'Required'),\n        description: z.string().min(1, 'Required'),\n        type: z.string().min(1, 'Required'),\n      })}\n      id=\"my-form\"\n    >\n      {({ register, formState }) => (\n        <>\n          <Input\n            label=\"Title\"\n            error={formState.errors['title']}\n            registration={register('title')}\n          />\n          <Textarea\n            label=\"Description\"\n            error={formState.errors['description']}\n            registration={register('description')}\n          />\n          <Select\n            label=\"Type\"\n            error={formState.errors['type']}\n            registration={register('type')}\n            options={['A', 'B', 'C'].map((type) => ({\n              label: type,\n              value: type,\n            }))}\n          />\n\n          {!hideSubmit && (\n            <div>\n              <Button type=\"submit\" className=\"w-full\">\n                Submit\n              </Button>\n            </div>\n          )}\n        </>\n      )}\n    </Form>\n  );\n};\n\nconst meta: Meta = {\n  component: MyForm,\n};\n\nexport default meta;\n\ntype Story = StoryObj<typeof MyForm>;\n\nexport const Default: Story = {\n  render: () => <MyForm />,\n};\n\nexport const AsFormDrawer: Story = {\n  render: () => (\n    <FormDrawer\n      triggerButton={<Button>Open Form</Button>}\n      isDone={true}\n      title=\"My Form\"\n      submitButton={\n        <Button form=\"my-form\" type=\"submit\">\n          Submit\n        </Button>\n      }\n    >\n      <MyForm hideSubmit />\n    </FormDrawer>\n  ),\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/form/form.tsx",
    "content": "import { zodResolver } from '@hookform/resolvers/zod';\nimport * as LabelPrimitive from '@radix-ui/react-label';\nimport { Slot } from '@radix-ui/react-slot';\nimport * as React from 'react';\nimport {\n  Controller,\n  ControllerProps,\n  FieldPath,\n  FieldValues,\n  FormProvider,\n  SubmitHandler,\n  UseFormProps,\n  UseFormReturn,\n  useForm,\n  useFormContext,\n} from 'react-hook-form';\nimport { ZodType, z } from 'zod';\n\nimport { cn } from '@/utils/cn';\n\nimport { Label } from './label';\n\ntype FormFieldContextValue<\n  TFieldValues extends FieldValues = FieldValues,\n  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n> = {\n  name: TName;\n};\n\nconst FormFieldContext = React.createContext<FormFieldContextValue>(\n  {} as FormFieldContextValue,\n);\n\nconst FormField = <\n  TFieldValues extends FieldValues,\n  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n>({\n  ...props\n}: ControllerProps<TFieldValues, TName>) => {\n  return (\n    <FormFieldContext.Provider value={{ name: props.name }}>\n      <Controller {...props} />\n    </FormFieldContext.Provider>\n  );\n};\n\nconst useFormField = () => {\n  const fieldContext = React.useContext(FormFieldContext);\n  const itemContext = React.useContext(FormItemContext);\n  const { getFieldState, formState } = useFormContext();\n\n  const fieldState = getFieldState(fieldContext.name, formState);\n\n  if (!fieldContext) {\n    throw new Error('useFormField should be used within <FormField>');\n  }\n\n  const { id } = itemContext;\n\n  return {\n    id,\n    name: fieldContext.name,\n    formItemId: `${id}-form-item`,\n    formDescriptionId: `${id}-form-item-description`,\n    formMessageId: `${id}-form-item-message`,\n    ...fieldState,\n  };\n};\n\ntype FormItemContextValue = {\n  id: string;\n};\n\nconst FormItemContext = React.createContext<FormItemContextValue>(\n  {} as FormItemContextValue,\n);\n\nconst FormItem = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => {\n  const id = React.useId();\n\n  return (\n    <FormItemContext.Provider value={{ id }}>\n      <div ref={ref} className={cn('space-y-2', className)} {...props} />\n    </FormItemContext.Provider>\n  );\n});\nFormItem.displayName = 'FormItem';\n\nconst FormLabel = React.forwardRef<\n  React.ElementRef<typeof LabelPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>\n>(({ className, ...props }, ref) => {\n  const { error, formItemId } = useFormField();\n\n  return (\n    <Label\n      ref={ref}\n      className={cn(error && 'text-destructive', className)}\n      htmlFor={formItemId}\n      {...props}\n    />\n  );\n});\nFormLabel.displayName = 'FormLabel';\n\nconst FormControl = React.forwardRef<\n  React.ElementRef<typeof Slot>,\n  React.ComponentPropsWithoutRef<typeof Slot>\n>(({ ...props }, ref) => {\n  const { error, formItemId, formDescriptionId, formMessageId } =\n    useFormField();\n\n  return (\n    <Slot\n      ref={ref}\n      id={formItemId}\n      aria-describedby={\n        !error\n          ? `${formDescriptionId}`\n          : `${formDescriptionId} ${formMessageId}`\n      }\n      aria-invalid={!!error}\n      {...props}\n    />\n  );\n});\nFormControl.displayName = 'FormControl';\n\nconst FormDescription = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, ...props }, ref) => {\n  const { formDescriptionId } = useFormField();\n\n  return (\n    <p\n      ref={ref}\n      id={formDescriptionId}\n      className={cn('text-[0.8rem] text-muted-foreground', className)}\n      {...props}\n    />\n  );\n});\nFormDescription.displayName = 'FormDescription';\n\nconst FormMessage = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, children, ...props }, ref) => {\n  const { error, formMessageId } = useFormField();\n  const body = error ? String(error?.message) : children;\n\n  if (!body) {\n    return null;\n  }\n\n  return (\n    <p\n      ref={ref}\n      id={formMessageId}\n      className={cn('text-[0.8rem] font-medium text-destructive', className)}\n      {...props}\n    >\n      {body}\n    </p>\n  );\n});\nFormMessage.displayName = 'FormMessage';\n\ntype FormProps<TFormValues extends FieldValues, Schema> = {\n  onSubmit: SubmitHandler<TFormValues>;\n  schema: Schema;\n  className?: string;\n  children: (methods: UseFormReturn<TFormValues>) => React.ReactNode;\n  options?: UseFormProps<TFormValues>;\n  id?: string;\n};\n\nconst Form = <\n  Schema extends ZodType<any, any, any>,\n  TFormValues extends FieldValues = z.infer<Schema>,\n>({\n  onSubmit,\n  children,\n  className,\n  options,\n  id,\n  schema,\n}: FormProps<TFormValues, Schema>) => {\n  const form = useForm({ ...options, resolver: zodResolver(schema) });\n  return (\n    <FormProvider {...form}>\n      <form\n        className={cn('space-y-6', className)}\n        onSubmit={form.handleSubmit(onSubmit)}\n        id={id}\n      >\n        {children(form)}\n      </form>\n    </FormProvider>\n  );\n};\n\nexport {\n  useFormField,\n  Form,\n  FormProvider,\n  FormItem,\n  FormLabel,\n  FormControl,\n  FormDescription,\n  FormMessage,\n  FormField,\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/form/index.ts",
    "content": "export * from './form';\nexport * from './input';\nexport * from './select';\nexport * from './textarea';\nexport * from './form-drawer';\nexport * from './label';\nexport * from './switch';\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/form/input.tsx",
    "content": "import * as React from 'react';\nimport { type UseFormRegisterReturn } from 'react-hook-form';\n\nimport { cn } from '@/utils/cn';\n\nimport { FieldWrapper, FieldWrapperPassThroughProps } from './field-wrapper';\n\nexport type InputProps = React.InputHTMLAttributes<HTMLInputElement> &\n  FieldWrapperPassThroughProps & {\n    className?: string;\n    registration: Partial<UseFormRegisterReturn>;\n  };\n\nconst Input = React.forwardRef<HTMLInputElement, InputProps>(\n  ({ className, type, label, error, registration, ...props }, ref) => {\n    return (\n      <FieldWrapper label={label} error={error}>\n        <input\n          type={type}\n          className={cn(\n            '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',\n            className,\n          )}\n          ref={ref}\n          {...registration}\n          {...props}\n        />\n      </FieldWrapper>\n    );\n  },\n);\nInput.displayName = 'Input';\n\nexport { Input };\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/form/label.tsx",
    "content": "import * as LabelPrimitive from '@radix-ui/react-label';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport * as React from 'react';\n\nimport { cn } from '@/utils/cn';\n\nconst labelVariants = cva(\n  'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',\n);\n\nconst Label = React.forwardRef<\n  React.ElementRef<typeof LabelPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &\n    VariantProps<typeof labelVariants>\n>(({ className, ...props }, ref) => (\n  <LabelPrimitive.Root\n    ref={ref}\n    className={cn(labelVariants(), className)}\n    {...props}\n  />\n));\nLabel.displayName = LabelPrimitive.Root.displayName;\n\nexport { Label };\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/form/select.tsx",
    "content": "import * as React from 'react';\nimport { UseFormRegisterReturn } from 'react-hook-form';\n\nimport { cn } from '@/utils/cn';\n\nimport { FieldWrapper, FieldWrapperPassThroughProps } from './field-wrapper';\n\ntype Option = {\n  label: React.ReactNode;\n  value: string | number | string[];\n};\n\ntype SelectFieldProps = FieldWrapperPassThroughProps & {\n  options: Option[];\n  className?: string;\n  defaultValue?: string;\n  registration: Partial<UseFormRegisterReturn>;\n};\n\nexport const Select = (props: SelectFieldProps) => {\n  const { label, options, error, className, defaultValue, registration } =\n    props;\n  return (\n    <FieldWrapper label={label} error={error}>\n      <select\n        className={cn(\n          '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',\n          className,\n        )}\n        defaultValue={defaultValue}\n        {...registration}\n      >\n        {options.map(({ label, value }) => (\n          <option key={label?.toString()} value={value}>\n            {label}\n          </option>\n        ))}\n      </select>\n    </FieldWrapper>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/form/switch.tsx",
    "content": "import * as SwitchPrimitives from '@radix-ui/react-switch';\nimport * as React from 'react';\n\nimport { cn } from '@/utils/cn';\n\nconst Switch = React.forwardRef<\n  React.ElementRef<typeof SwitchPrimitives.Root>,\n  React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>\n>(({ className, ...props }, ref) => (\n  <SwitchPrimitives.Root\n    className={cn(\n      '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',\n      className,\n    )}\n    {...props}\n    ref={ref}\n  >\n    <SwitchPrimitives.Thumb\n      className={cn(\n        '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',\n      )}\n    />\n  </SwitchPrimitives.Root>\n));\nSwitch.displayName = SwitchPrimitives.Root.displayName;\n\nexport { Switch };\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/form/textarea.tsx",
    "content": "import * as React from 'react';\nimport { UseFormRegisterReturn } from 'react-hook-form';\n\nimport { cn } from '@/utils/cn';\n\nimport { FieldWrapper, FieldWrapperPassThroughProps } from './field-wrapper';\n\nexport type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement> &\n  FieldWrapperPassThroughProps & {\n    className?: string;\n    registration: Partial<UseFormRegisterReturn>;\n  };\n\nconst Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(\n  ({ className, label, error, registration, ...props }, ref) => {\n    return (\n      <FieldWrapper label={label} error={error}>\n        <textarea\n          className={cn(\n            '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',\n            className,\n          )}\n          ref={ref}\n          {...registration}\n          {...props}\n        />\n      </FieldWrapper>\n    );\n  },\n);\nTextarea.displayName = 'Textarea';\n\nexport { Textarea };\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/link/index.ts",
    "content": "export * from './link';\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/link/link.stories.tsx",
    "content": "import { Meta, StoryObj } from '@storybook/react';\n\nimport { Link } from './link';\n\nconst meta: Meta<typeof Link> = {\n  component: Link,\n};\n\nexport default meta;\n\ntype Story = StoryObj<typeof Link>;\n\nexport const Default: Story = {\n  args: {\n    children: 'Link',\n    href: '/',\n  },\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/link/link.tsx",
    "content": "import NextLink, { LinkProps as NextLinkProps } from 'next/link';\n\nimport { cn } from '@/utils/cn';\n\nexport type LinkProps = {\n  className?: string;\n  children: React.ReactNode;\n  target?: string;\n} & NextLinkProps;\n\nexport const Link = ({ className, children, href, ...props }: LinkProps) => {\n  return (\n    <NextLink\n      href={href}\n      className={cn('text-slate-600 hover:text-slate-900', className)}\n      {...props}\n    >\n      {children}\n    </NextLink>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/md-preview/index.ts",
    "content": "export * from './md-preview';\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/md-preview/md-preview.stories.tsx",
    "content": "import { Meta, StoryObj } from '@storybook/react';\n\nimport { MDPreview } from './md-preview';\n\nconst meta: Meta<typeof MDPreview> = {\n  component: MDPreview,\n};\n\nexport default meta;\n\ntype Story = StoryObj<typeof MDPreview>;\n\nexport const Default: Story = {\n  args: {\n    value: `## Hello World!`,\n  },\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/md-preview/md-preview.tsx",
    "content": "import DOMPurify from 'isomorphic-dompurify';\nimport { parse } from 'marked';\n\nexport type MDPreviewProps = {\n  value: string;\n};\n\nexport const MDPreview = ({ value = '' }: MDPreviewProps) => {\n  return (\n    <div\n      className=\"prose prose-slate w-full p-2\"\n      dangerouslySetInnerHTML={{\n        __html: DOMPurify.sanitize(parse(value) as string),\n      }}\n    />\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/notifications/__tests__/notifications.test.ts",
    "content": "import { renderHook, act } from '@testing-library/react';\n\nimport { useNotifications, Notification } from '../notifications-store';\n\ntest('should add and remove notifications', () => {\n  const { result } = renderHook(() => useNotifications());\n\n  expect(result.current.notifications.length).toBe(0);\n\n  const notification: Notification = {\n    id: '123',\n    title: 'Hello World',\n    type: 'info',\n    message: 'This is a notification',\n  };\n\n  act(() => {\n    result.current.addNotification(notification);\n  });\n\n  expect(result.current.notifications).toContainEqual(notification);\n\n  act(() => {\n    result.current.dismissNotification(notification.id);\n  });\n\n  expect(result.current.notifications).not.toContainEqual(notification);\n});\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/notifications/index.ts",
    "content": "export * from './notifications';\nexport * from './notifications-store';\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/notifications/notification.stories.tsx",
    "content": "import { Meta, StoryObj } from '@storybook/react';\n\nimport { Notification } from './notification';\n\nconst meta: Meta<typeof Notification> = {\n  title: 'Components/Notifications',\n  component: Notification,\n  parameters: {\n    controls: { expanded: true },\n  },\n};\n\nexport default meta;\n\ntype Story = StoryObj<typeof Notification>;\n\nexport const Info: Story = {\n  args: {\n    notification: {\n      id: '1',\n      type: 'info',\n      title: 'Hello Info',\n      message: 'This is info notification',\n    },\n    onDismiss: (id) => alert(`Dismissing Notification with id: ${id}`),\n  },\n};\n\nexport const Success: Story = {\n  args: {\n    notification: {\n      id: '1',\n      type: 'success',\n      title: 'Hello Success',\n      message: 'This is success notification',\n    },\n    onDismiss: (id) => alert(`Dismissing Notification with id: ${id}`),\n  },\n};\n\nexport const Warning: Story = {\n  args: {\n    notification: {\n      id: '1',\n      type: 'warning',\n      title: 'Hello Warning',\n      message: 'This is warning notification',\n    },\n    onDismiss: (id) => alert(`Dismissing Notification with id: ${id}`),\n  },\n};\n\nexport const Error: Story = {\n  args: {\n    notification: {\n      id: '1',\n      type: 'error',\n      title: 'Hello Error',\n      message: 'This is error notification',\n    },\n    onDismiss: (id) => alert(`Dismissing Notification with id: ${id}`),\n  },\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/notifications/notification.tsx",
    "content": "import { Info, CircleAlert, CircleX, CircleCheck } from 'lucide-react';\n\nconst icons = {\n  info: <Info className=\"size-6 text-blue-500\" aria-hidden=\"true\" />,\n  success: <CircleCheck className=\"size-6 text-green-500\" aria-hidden=\"true\" />,\n  warning: (\n    <CircleAlert className=\"size-6 text-yellow-500\" aria-hidden=\"true\" />\n  ),\n  error: <CircleX className=\"size-6 text-red-500\" aria-hidden=\"true\" />,\n};\n\nexport type NotificationProps = {\n  notification: {\n    id: string;\n    type: keyof typeof icons;\n    title: string;\n    message?: string;\n  };\n  onDismiss: (id: string) => void;\n};\n\nexport const Notification = ({\n  notification: { id, type, title, message },\n  onDismiss,\n}: NotificationProps) => {\n  return (\n    <div className=\"flex w-full flex-col items-center space-y-4 sm:items-end\">\n      <div className=\"pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg bg-white shadow-lg ring-1 ring-black/5\">\n        <div className=\"p-4\" role=\"alert\" aria-label={title}>\n          <div className=\"flex items-start\">\n            <div className=\"shrink-0\">{icons[type]}</div>\n            <div className=\"ml-3 w-0 flex-1 pt-0.5\">\n              <p className=\"text-sm font-medium text-gray-900\">{title}</p>\n              <p className=\"mt-1 text-sm text-gray-500\">{message}</p>\n            </div>\n            <div className=\"ml-4 flex shrink-0\">\n              <button\n                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\"\n                onClick={() => {\n                  onDismiss(id);\n                }}\n              >\n                <span className=\"sr-only\">Close</span>\n                <CircleX className=\"size-5\" aria-hidden=\"true\" />\n              </button>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/notifications/notifications-store.ts",
    "content": "import { nanoid } from 'nanoid';\nimport { create } from 'zustand';\n\nexport type Notification = {\n  id: string;\n  type: 'info' | 'warning' | 'success' | 'error';\n  title: string;\n  message?: string;\n};\n\ntype NotificationsStore = {\n  notifications: Notification[];\n  addNotification: (notification: Omit<Notification, 'id'>) => void;\n  dismissNotification: (id: string) => void;\n};\n\nexport const useNotifications = create<NotificationsStore>((set) => ({\n  notifications: [],\n  addNotification: (notification) =>\n    set((state) => ({\n      notifications: [\n        ...state.notifications,\n        { id: nanoid(), ...notification },\n      ],\n    })),\n  dismissNotification: (id) =>\n    set((state) => ({\n      notifications: state.notifications.filter(\n        (notification) => notification.id !== id,\n      ),\n    })),\n}));\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/notifications/notifications.tsx",
    "content": "import { Notification } from './notification';\nimport { useNotifications } from './notifications-store';\n\nexport const Notifications = () => {\n  const { notifications, dismissNotification } = useNotifications();\n\n  return (\n    <div\n      aria-live=\"assertive\"\n      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\"\n    >\n      {notifications.map((notification) => (\n        <Notification\n          key={notification.id}\n          notification={notification}\n          onDismiss={dismissNotification}\n        />\n      ))}\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/spinner/index.ts",
    "content": "export * from './spinner';\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/spinner/spinner.stories.tsx",
    "content": "import { Meta, StoryObj } from '@storybook/react';\n\nimport { Spinner } from './spinner';\n\nconst meta: Meta<typeof Spinner> = {\n  component: Spinner,\n};\n\nexport default meta;\n\ntype Story = StoryObj<typeof Spinner>;\n\nexport const Default: Story = {\n  args: {\n    size: 'md',\n  },\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/spinner/spinner.tsx",
    "content": "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',\n};\n\nconst variants = {\n  light: 'text-white',\n  primary: 'text-slate-600',\n};\n\nexport type SpinnerProps = {\n  size?: keyof typeof sizes;\n  variant?: keyof typeof variants;\n  className?: string;\n};\n\nexport const Spinner = ({\n  size = 'md',\n  variant = 'primary',\n  className = '',\n}: SpinnerProps) => {\n  return (\n    <>\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        width=\"24\"\n        height=\"24\"\n        viewBox=\"0 0 24 24\"\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeWidth=\"2\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        className={cn(\n          'animate-spin',\n          sizes[size],\n          variants[variant],\n          className,\n        )}\n      >\n        <path d=\"M21 12a9 9 0 1 1-6.219-8.56\" />\n      </svg>\n      <span className=\"sr-only\">Loading</span>\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/table/index.ts",
    "content": "export * from './table';\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/table/pagination.tsx",
    "content": "import {\n  ChevronLeftIcon,\n  ChevronRightIcon,\n  DotsHorizontalIcon,\n} from '@radix-ui/react-icons';\nimport * as React from 'react';\n\nimport { ButtonProps, buttonVariants } from '@/components/ui/button';\nimport { cn } from '@/utils/cn';\n\nimport { Link } from '../link';\n\nconst Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => (\n  <nav\n    role=\"navigation\"\n    aria-label=\"pagination\"\n    className={cn('mx-auto flex w-full justify-center', className)}\n    {...props}\n  />\n);\nPagination.displayName = 'Pagination';\n\nconst PaginationContent = React.forwardRef<\n  HTMLUListElement,\n  React.ComponentProps<'ul'>\n>(({ className, ...props }, ref) => (\n  <ul\n    ref={ref}\n    className={cn('flex flex-row items-center gap-1', className)}\n    {...props}\n  />\n));\nPaginationContent.displayName = 'PaginationContent';\n\nconst PaginationItem = React.forwardRef<\n  HTMLLIElement,\n  React.ComponentProps<'li'>\n>(({ className, ...props }, ref) => (\n  <li ref={ref} className={cn('', className)} {...props} />\n));\nPaginationItem.displayName = 'PaginationItem';\n\ntype PaginationLinkProps = {\n  isActive?: boolean;\n} & Pick<ButtonProps, 'size'> &\n  React.ComponentProps<'a'>;\n\nconst PaginationLink = ({\n  className,\n  isActive,\n  size = 'icon',\n  children,\n  href,\n  ...props\n}: PaginationLinkProps) => (\n  <Link\n    href={href as string}\n    aria-current={isActive ? 'page' : undefined}\n    className={cn(\n      buttonVariants({\n        variant: isActive ? 'outline' : 'ghost',\n        size,\n      }),\n      className,\n    )}\n    {...props}\n  >\n    {children}\n  </Link>\n);\nPaginationLink.displayName = 'PaginationLink';\n\nconst PaginationPrevious = ({\n  className,\n  ...props\n}: React.ComponentProps<typeof PaginationLink>) => (\n  <PaginationLink\n    aria-label=\"Go to previous page\"\n    size=\"default\"\n    className={cn('gap-1 pl-2.5', className)}\n    {...props}\n  >\n    <ChevronLeftIcon className=\"size-4\" />\n    <span>Previous</span>\n  </PaginationLink>\n);\nPaginationPrevious.displayName = 'PaginationPrevious';\n\nconst PaginationNext = ({\n  className,\n  ...props\n}: React.ComponentProps<typeof PaginationLink>) => (\n  <PaginationLink\n    aria-label=\"Go to next page\"\n    size=\"default\"\n    className={cn('gap-1 pr-2.5', className)}\n    {...props}\n  >\n    <span>Next</span>\n    <ChevronRightIcon className=\"size-4\" />\n  </PaginationLink>\n);\nPaginationNext.displayName = 'PaginationNext';\n\nconst PaginationEllipsis = ({\n  className,\n  ...props\n}: React.ComponentProps<'span'>) => (\n  <span\n    aria-hidden\n    className={cn('flex h-9 w-9 items-center justify-center', className)}\n    {...props}\n  >\n    <DotsHorizontalIcon className=\"size-4\" />\n    <span className=\"sr-only\">More pages</span>\n  </span>\n);\nPaginationEllipsis.displayName = 'PaginationEllipsis';\n\nexport {\n  Pagination,\n  PaginationContent,\n  PaginationLink,\n  PaginationItem,\n  PaginationPrevious,\n  PaginationNext,\n  PaginationEllipsis,\n};\n\nexport type TablePaginationProps = {\n  totalPages: number;\n  currentPage: number;\n  rootUrl: string;\n};\n\nexport const TablePagination = ({\n  totalPages,\n  currentPage,\n  rootUrl,\n}: TablePaginationProps) => {\n  const createHref = (page: number) => `${rootUrl}?page=${page}`;\n\n  return (\n    <Pagination className=\"justify-end py-8\">\n      <PaginationContent>\n        {currentPage > 1 && (\n          <PaginationItem>\n            <PaginationPrevious href={createHref(currentPage - 1)} />\n          </PaginationItem>\n        )}\n        {currentPage > 2 && (\n          <PaginationItem>\n            <PaginationEllipsis />\n          </PaginationItem>\n        )}\n        {currentPage > 1 && (\n          <PaginationItem>\n            <PaginationLink href={createHref(currentPage - 1)}>\n              {currentPage - 1}\n            </PaginationLink>\n          </PaginationItem>\n        )}\n        <PaginationItem className=\"rounded-sm bg-gray-200\">\n          <PaginationLink href={createHref(currentPage)}>\n            {currentPage}\n          </PaginationLink>\n        </PaginationItem>\n        {totalPages > currentPage && (\n          <PaginationItem>\n            <PaginationLink href={createHref(currentPage + 1)}>\n              {currentPage + 1}\n            </PaginationLink>\n          </PaginationItem>\n        )}\n        {totalPages > currentPage + 1 && (\n          <PaginationItem>\n            <PaginationEllipsis />\n          </PaginationItem>\n        )}\n        {currentPage < totalPages && (\n          <PaginationItem>\n            <PaginationNext href={createHref(totalPages)} />\n          </PaginationItem>\n        )}\n      </PaginationContent>\n    </Pagination>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/table/table.stories.tsx",
    "content": "import { Meta, StoryObj } from '@storybook/react';\n\nimport { Table } from './table';\n\nconst meta: Meta<typeof Table> = {\n  component: Table,\n};\n\nexport default meta;\n\ntype User = {\n  id: string;\n  createdAt: number;\n  name: string;\n  title: string;\n  role: string;\n  email: string;\n};\n\ntype Story = StoryObj<typeof Table<User>>;\n\nconst data: User[] = [\n  {\n    id: '1',\n    createdAt: Date.now(),\n    name: 'Jane Cooper',\n    title: 'Regional Paradigm Technician',\n    role: 'Admin',\n    email: 'jane.cooper@example.com',\n  },\n  {\n    id: '2',\n    createdAt: Date.now(),\n    name: 'Cody Fisher',\n    title: 'Product Directives Officer',\n    role: 'Owner',\n    email: 'cody.fisher@example.com',\n  },\n];\n\nexport const Default: Story = {\n  args: {\n    data,\n    columns: [\n      {\n        title: 'Name',\n        field: 'name',\n      },\n      {\n        title: 'Title',\n        field: 'title',\n      },\n      {\n        title: 'Role',\n        field: 'role',\n      },\n      {\n        title: 'Email',\n        field: 'email',\n      },\n    ],\n  },\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/components/ui/table/table.tsx",
    "content": "import { ArchiveX } from 'lucide-react';\nimport * as React from 'react';\n\nimport { BaseEntity } from '@/types/api';\nimport { cn } from '@/utils/cn';\n\nimport { TablePagination, TablePaginationProps } from './pagination';\n\nconst TableElement = React.forwardRef<\n  HTMLTableElement,\n  React.HTMLAttributes<HTMLTableElement>\n>(({ className, ...props }, ref) => (\n  <div className=\"relative w-full overflow-auto\">\n    <table\n      ref={ref}\n      className={cn('w-full caption-bottom text-sm', className)}\n      {...props}\n    />\n  </div>\n));\nTableElement.displayName = 'Table';\n\nconst TableHeader = React.forwardRef<\n  HTMLTableSectionElement,\n  React.HTMLAttributes<HTMLTableSectionElement>\n>(({ className, ...props }, ref) => (\n  <thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />\n));\nTableHeader.displayName = 'TableHeader';\n\nconst TableBody = React.forwardRef<\n  HTMLTableSectionElement,\n  React.HTMLAttributes<HTMLTableSectionElement>\n>(({ className, ...props }, ref) => (\n  <tbody\n    ref={ref}\n    className={cn('[&_tr:last-child]:border-0', className)}\n    {...props}\n  />\n));\nTableBody.displayName = 'TableBody';\n\nconst TableFooter = React.forwardRef<\n  HTMLTableSectionElement,\n  React.HTMLAttributes<HTMLTableSectionElement>\n>(({ className, ...props }, ref) => (\n  <tfoot\n    ref={ref}\n    className={cn(\n      'border-t bg-muted/50 font-medium [&>tr]:last:border-b-0',\n      className,\n    )}\n    {...props}\n  />\n));\nTableFooter.displayName = 'TableFooter';\n\nconst TableRow = React.forwardRef<\n  HTMLTableRowElement,\n  React.HTMLAttributes<HTMLTableRowElement>\n>(({ className, ...props }, ref) => (\n  <tr\n    ref={ref}\n    className={cn(\n      'border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted',\n      className,\n    )}\n    {...props}\n  />\n));\nTableRow.displayName = 'TableRow';\n\nconst TableHead = React.forwardRef<\n  HTMLTableCellElement,\n  React.ThHTMLAttributes<HTMLTableCellElement>\n>(({ className, ...props }, ref) => (\n  <th\n    ref={ref}\n    className={cn(\n      'h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',\n      className,\n    )}\n    {...props}\n  />\n));\nTableHead.displayName = 'TableHead';\n\nconst TableCell = React.forwardRef<\n  HTMLTableCellElement,\n  React.TdHTMLAttributes<HTMLTableCellElement>\n>(({ className, ...props }, ref) => (\n  <td\n    ref={ref}\n    className={cn(\n      'p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',\n      className,\n    )}\n    {...props}\n  />\n));\nTableCell.displayName = 'TableCell';\n\nconst TableCaption = React.forwardRef<\n  HTMLTableCaptionElement,\n  React.HTMLAttributes<HTMLTableCaptionElement>\n>(({ className, ...props }, ref) => (\n  <caption\n    ref={ref}\n    className={cn('mt-4 text-sm text-muted-foreground', className)}\n    {...props}\n  />\n));\nTableCaption.displayName = 'TableCaption';\n\nexport {\n  TableElement,\n  TableHeader,\n  TableBody,\n  TableFooter,\n  TableHead,\n  TableRow,\n  TableCell,\n  TableCaption,\n};\n\ntype TableColumn<Entry> = {\n  title: string;\n  field: keyof Entry;\n  Cell?({ entry }: { entry: Entry }): React.ReactElement;\n};\n\nexport type TableProps<Entry> = {\n  data: Entry[];\n  columns: TableColumn<Entry>[];\n  pagination?: TablePaginationProps;\n};\n\nexport const Table = <Entry extends BaseEntity>({\n  data,\n  columns,\n  pagination,\n}: TableProps<Entry>) => {\n  if (!data?.length) {\n    return (\n      <div className=\"flex h-80 flex-col items-center justify-center bg-white text-gray-500\">\n        <ArchiveX className=\"size-16\" />\n        <h4>No Entries Found</h4>\n      </div>\n    );\n  }\n  return (\n    <>\n      <TableElement>\n        <TableHeader>\n          <TableRow>\n            {columns.map((column, index) => (\n              <TableHead key={column.title + index}>{column.title}</TableHead>\n            ))}\n          </TableRow>\n        </TableHeader>\n        <TableBody>\n          {data.map((entry, entryIndex) => (\n            <TableRow key={entry?.id || entryIndex}>\n              {columns.map(({ Cell, field, title }, columnIndex) => (\n                <TableCell key={title + columnIndex}>\n                  {Cell ? <Cell entry={entry} /> : `${entry[field]}`}\n                </TableCell>\n              ))}\n            </TableRow>\n          ))}\n        </TableBody>\n      </TableElement>\n\n      {pagination && <TablePagination {...pagination} />}\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/config/env.ts",
    "content": "import * as z from 'zod';\nimport 'dotenv/config';\n\nconst createEnv = () => {\n  const EnvSchema = z.object({\n    API_URL: z.string(),\n    ENABLE_API_MOCKING: z\n      .string()\n      .refine((s) => s === 'true' || s === 'false')\n      .transform((s) => s === 'true')\n      .optional(),\n    APP_URL: z.string().optional().default('http://localhost:3000'),\n    APP_MOCK_API_PORT: z.string().optional().default('8080'),\n  });\n\n  const envVars = {\n    API_URL: process.env.NEXT_PUBLIC_API_URL,\n    ENABLE_API_MOCKING: process.env.NEXT_PUBLIC_ENABLE_API_MOCKING,\n    APP_URL: process.env.NEXT_PUBLIC_URL,\n    APP_MOCK_API_PORT: process.env.NEXT_PUBLIC_MOCK_API_PORT,\n  };\n\n  const parsedEnv = EnvSchema.safeParse(envVars);\n\n  if (!parsedEnv.success) {\n    throw new Error(\n      `Invalid env provided.\n  The following variables are missing or invalid:\n  ${Object.entries(parsedEnv.error.flatten().fieldErrors)\n    .map(([k, v]) => `- ${k}: ${v}`)\n    .join('\\n')}\n  `,\n    );\n  }\n\n  return parsedEnv.data ?? {};\n};\n\nexport const env = createEnv();\n"
  },
  {
    "path": "apps/nextjs-pages/src/config/paths.ts",
    "content": "export const paths = {\n  home: {\n    getHref: () => '/',\n  },\n\n  auth: {\n    register: {\n      getHref: (redirectTo?: string | null | undefined) =>\n        `/auth/register${redirectTo ? `?redirectTo=${encodeURIComponent(redirectTo)}` : ''}`,\n    },\n    login: {\n      getHref: (redirectTo?: string | null | undefined) =>\n        `/auth/login${redirectTo ? `?redirectTo=${encodeURIComponent(redirectTo)}` : ''}`,\n    },\n  },\n\n  app: {\n    root: {\n      getHref: () => '/app',\n    },\n    dashboard: {\n      getHref: () => '/app',\n    },\n    discussions: {\n      getHref: () => '/app/discussions',\n    },\n    discussion: {\n      getHref: (id: string) => `/app/discussions/${id}`,\n    },\n    users: {\n      getHref: () => '/app/users',\n    },\n    profile: {\n      getHref: () => '/app/profile',\n    },\n  },\n  public: {\n    discussion: {\n      getHref: (id: string) => `/public/discussions/${id}`,\n    },\n  },\n} as const;\n"
  },
  {
    "path": "apps/nextjs-pages/src/features/auth/components/__tests__/login-form.test.tsx",
    "content": "import {\n  createUser,\n  renderApp,\n  screen,\n  userEvent,\n  waitFor,\n} from '@/testing/test-utils';\n\nimport { LoginForm } from '../login-form';\n\ntest('should login new user and call onSuccess cb which should navigate the user to the app', async () => {\n  const newUser = await createUser({ teamId: undefined });\n\n  const onSuccess = vi.fn();\n\n  await renderApp(<LoginForm onSuccess={onSuccess} />, { user: null });\n\n  await userEvent.type(screen.getByLabelText(/email address/i), newUser.email);\n  await userEvent.type(screen.getByLabelText(/password/i), newUser.password);\n\n  await userEvent.click(screen.getByRole('button', { name: /log in/i }));\n\n  await waitFor(() => expect(onSuccess).toHaveBeenCalledTimes(1));\n});\n"
  },
  {
    "path": "apps/nextjs-pages/src/features/auth/components/__tests__/register-form.test.tsx",
    "content": "import { createUser } from '@/testing/data-generators';\nimport { renderApp, screen, userEvent, waitFor } from '@/testing/test-utils';\n\nimport { RegisterForm } from '../register-form';\n\ntest('should register new user and call onSuccess cb which should navigate the user to the app', async () => {\n  const newUser = createUser({});\n\n  const onSuccess = vi.fn();\n\n  await renderApp(\n    <RegisterForm\n      onSuccess={onSuccess}\n      chooseTeam={false}\n      setChooseTeam={() => {}}\n      teams={[]}\n    />,\n    { user: null },\n  );\n\n  await userEvent.type(screen.getByLabelText(/first name/i), newUser.firstName);\n  await userEvent.type(screen.getByLabelText(/last name/i), newUser.lastName);\n  await userEvent.type(screen.getByLabelText(/email address/i), newUser.email);\n  await userEvent.type(screen.getByLabelText(/password/i), newUser.password);\n  await userEvent.type(screen.getByLabelText(/team name/i), newUser.teamName);\n\n  await userEvent.click(screen.getByRole('button', { name: /register/i }));\n\n  await waitFor(() => expect(onSuccess).toHaveBeenCalledTimes(1));\n});\n"
  },
  {
    "path": "apps/nextjs-pages/src/features/auth/components/login-form.tsx",
    "content": "import NextLink from 'next/link';\nimport { useRouter } from 'next/router';\n\nimport { Button } from '@/components/ui/button';\nimport { Form, Input } from '@/components/ui/form';\nimport { paths } from '@/config/paths';\nimport { useLogin, loginInputSchema } from '@/lib/auth';\n\ntype LoginFormProps = {\n  onSuccess: () => void;\n};\n\nexport const LoginForm = ({ onSuccess }: LoginFormProps) => {\n  const login = useLogin({\n    onSuccess,\n  });\n  const router = useRouter();\n  const redirectTo = router.query.redirectTo as string | undefined;\n\n  return (\n    <div>\n      <Form\n        onSubmit={(values) => {\n          login.mutate(values);\n        }}\n        schema={loginInputSchema}\n      >\n        {({ register, formState }) => (\n          <>\n            <Input\n              type=\"email\"\n              label=\"Email Address\"\n              error={formState.errors['email']}\n              registration={register('email')}\n            />\n            <Input\n              type=\"password\"\n              label=\"Password\"\n              error={formState.errors['password']}\n              registration={register('password')}\n            />\n            <div>\n              <Button\n                isLoading={login.isPending}\n                type=\"submit\"\n                className=\"w-full\"\n              >\n                Log in\n              </Button>\n            </div>\n          </>\n        )}\n      </Form>\n      <div className=\"mt-2 flex items-center justify-end\">\n        <div className=\"text-sm\">\n          <NextLink\n            href={paths.auth.register.getHref(redirectTo)}\n            className=\"font-medium text-blue-600 hover:text-blue-500\"\n          >\n            Register\n          </NextLink>\n        </div>\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/features/auth/components/register-form.tsx",
    "content": "import NextLink from 'next/link';\nimport { useRouter } from 'next/router';\nimport * as React from 'react';\n\nimport { Button } from '@/components/ui/button';\nimport { Form, Input, Select, Label, Switch } from '@/components/ui/form';\nimport { paths } from '@/config/paths';\nimport { useRegister, registerInputSchema } from '@/lib/auth';\nimport { Team } from '@/types/api';\n\ntype RegisterFormProps = {\n  onSuccess: () => void;\n  chooseTeam: boolean;\n  setChooseTeam: () => void;\n  teams?: Team[];\n};\n\nexport const RegisterForm = ({\n  onSuccess,\n  chooseTeam,\n  setChooseTeam,\n  teams,\n}: RegisterFormProps) => {\n  const registering = useRegister({ onSuccess });\n  const router = useRouter();\n  const redirectTo = router.query.redirectTo as string | undefined;\n\n  return (\n    <div>\n      <Form\n        onSubmit={(values) => {\n          registering.mutate(values);\n        }}\n        schema={registerInputSchema}\n        options={{\n          shouldUnregister: true,\n        }}\n      >\n        {({ register, formState }) => (\n          <>\n            <Input\n              type=\"text\"\n              label=\"First Name\"\n              error={formState.errors['firstName']}\n              registration={register('firstName')}\n            />\n            <Input\n              type=\"text\"\n              label=\"Last Name\"\n              error={formState.errors['lastName']}\n              registration={register('lastName')}\n            />\n            <Input\n              type=\"email\"\n              label=\"Email Address\"\n              error={formState.errors['email']}\n              registration={register('email')}\n            />\n            <Input\n              type=\"password\"\n              label=\"Password\"\n              error={formState.errors['password']}\n              registration={register('password')}\n            />\n\n            <div className=\"flex items-center space-x-2\">\n              <Switch\n                checked={chooseTeam}\n                onCheckedChange={setChooseTeam}\n                className={`${\n                  chooseTeam ? 'bg-blue-600' : 'bg-gray-200'\n                } 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`}\n                id=\"choose-team\"\n              />\n              <Label htmlFor=\"airplane-mode\">Join Existing Team</Label>\n            </div>\n\n            {chooseTeam && teams ? (\n              <Select\n                label=\"Team\"\n                error={formState.errors['teamId']}\n                registration={register('teamId')}\n                options={teams?.map((team) => ({\n                  label: team.name,\n                  value: team.id,\n                }))}\n              />\n            ) : (\n              <Input\n                type=\"text\"\n                label=\"Team Name\"\n                error={formState.errors['teamName']}\n                registration={register('teamName')}\n              />\n            )}\n            <div>\n              <Button\n                isLoading={registering.isPending}\n                type=\"submit\"\n                className=\"w-full\"\n              >\n                Register\n              </Button>\n            </div>\n          </>\n        )}\n      </Form>\n      <div className=\"mt-2 flex items-center justify-end\">\n        <div className=\"text-sm\">\n          <NextLink\n            href={paths.auth.login.getHref(redirectTo)}\n            className=\"font-medium text-blue-600 hover:text-blue-500\"\n          >\n            Log In\n          </NextLink>\n        </div>\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/features/comments/api/create-comment.ts",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query';\nimport { z } from 'zod';\n\nimport { api } from '@/lib/api-client';\nimport { MutationConfig } from '@/lib/react-query';\nimport { Comment } from '@/types/api';\n\nimport { getInfiniteCommentsQueryOptions } from './get-comments';\n\nexport const createCommentInputSchema = z.object({\n  discussionId: z.string().min(1, 'Required'),\n  body: z.string().min(1, 'Required'),\n});\n\nexport type CreateCommentInput = z.infer<typeof createCommentInputSchema>;\n\nexport const createComment = ({\n  data,\n}: {\n  data: CreateCommentInput;\n}): Promise<Comment> => {\n  return api.post('/comments', data);\n};\n\ntype UseCreateCommentOptions = {\n  discussionId: string;\n  mutationConfig?: MutationConfig<typeof createComment>;\n};\n\nexport const useCreateComment = ({\n  mutationConfig,\n  discussionId,\n}: UseCreateCommentOptions) => {\n  const queryClient = useQueryClient();\n\n  const { onSuccess, ...restConfig } = mutationConfig || {};\n\n  return useMutation({\n    onSuccess: (...args) => {\n      queryClient.invalidateQueries({\n        queryKey: getInfiniteCommentsQueryOptions(discussionId).queryKey,\n      });\n      onSuccess?.(...args);\n    },\n    ...restConfig,\n    mutationFn: createComment,\n  });\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/features/comments/api/delete-comment.ts",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query';\n\nimport { api } from '@/lib/api-client';\nimport { MutationConfig } from '@/lib/react-query';\n\nimport { getInfiniteCommentsQueryOptions } from './get-comments';\n\nexport const deleteComment = ({ commentId }: { commentId: string }) => {\n  return api.delete(`/comments/${commentId}`);\n};\n\ntype UseDeleteCommentOptions = {\n  discussionId: string;\n  mutationConfig?: MutationConfig<typeof deleteComment>;\n};\n\nexport const useDeleteComment = ({\n  mutationConfig,\n  discussionId,\n}: UseDeleteCommentOptions) => {\n  const queryClient = useQueryClient();\n\n  const { onSuccess, ...restConfig } = mutationConfig || {};\n\n  return useMutation({\n    onSuccess: (...args) => {\n      queryClient.invalidateQueries({\n        queryKey: getInfiniteCommentsQueryOptions(discussionId).queryKey,\n      });\n      onSuccess?.(...args);\n    },\n    ...restConfig,\n    mutationFn: deleteComment,\n  });\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/features/comments/api/get-comments.ts",
    "content": "import { infiniteQueryOptions, useInfiniteQuery } from '@tanstack/react-query';\n\nimport { api, attachCookie } from '@/lib/api-client';\nimport { QueryConfig } from '@/lib/react-query';\nimport { Comment, Meta } from '@/types/api';\n\nexport const getComments = ({\n  discussionId,\n  page = 1,\n  cookie,\n}: {\n  discussionId: string;\n  page?: number;\n  cookie?: string;\n}): Promise<{ data: Comment[]; meta: Meta }> => {\n  return api.get(`/comments`, {\n    params: {\n      discussionId,\n      page,\n    },\n    headers: attachCookie(cookie).headers,\n  });\n};\n\nexport const getInfiniteCommentsQueryOptions = (\n  discussionId: string,\n  cookie?: string,\n) => {\n  return infiniteQueryOptions({\n    queryKey: ['comments', discussionId],\n    queryFn: ({ pageParam = 1 }) => {\n      return getComments({ discussionId, page: pageParam as number, cookie });\n    },\n    getNextPageParam: (lastPage) => {\n      if (lastPage?.meta?.page === lastPage?.meta?.totalPages) return undefined;\n      const nextPage = lastPage.meta.page + 1;\n      return nextPage;\n    },\n    initialPageParam: 1,\n  });\n};\n\ntype UseCommentsOptions = {\n  discussionId: string;\n  page?: number;\n  queryConfig?: QueryConfig<typeof getComments>;\n};\n\nexport const useInfiniteComments = ({ discussionId }: UseCommentsOptions) => {\n  return useInfiniteQuery({\n    ...getInfiniteCommentsQueryOptions(discussionId),\n  });\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/features/comments/components/comments-list.tsx",
    "content": "import { ArchiveX } from 'lucide-react';\nimport { usePathname } from 'next/navigation';\n\nimport { Button } from '@/components/ui/button';\nimport { MDPreview } from '@/components/ui/md-preview';\nimport { Spinner } from '@/components/ui/spinner';\nimport { useUser } from '@/lib/auth';\nimport { POLICIES, Authorization } from '@/lib/authorization';\nimport { User } from '@/types/api';\nimport { formatDate } from '@/utils/format';\n\nimport { useInfiniteComments } from '../api/get-comments';\n\nimport { DeleteComment } from './delete-comment';\n\ntype CommentsListProps = {\n  discussionId: string;\n};\n\nexport const CommentsList = ({ discussionId }: CommentsListProps) => {\n  const user = useUser();\n  const commentsQuery = useInfiniteComments({ discussionId });\n  const pathname = usePathname();\n  const isPublicView = pathname?.startsWith?.('/public/');\n\n  if (commentsQuery.isLoading) {\n    return (\n      <div className=\"flex h-48 w-full items-center justify-center\">\n        <Spinner size=\"lg\" />\n      </div>\n    );\n  }\n\n  const comments = commentsQuery.data?.pages.flatMap((page) => page.data);\n\n  if (!comments?.length)\n    return (\n      <div\n        role=\"list\"\n        aria-label=\"comments\"\n        className=\"flex h-40 flex-col items-center justify-center bg-white text-gray-500\"\n      >\n        <ArchiveX className=\"size-10\" />\n        <h4>No Comments Found</h4>\n      </div>\n    );\n\n  return (\n    <>\n      <ul aria-label=\"comments\" className=\"flex flex-col space-y-3\">\n        {comments.map((comment, index) => (\n          <li\n            aria-label={`comment-${comment.body}-${index}`}\n            key={comment.id || index}\n            className=\"w-full bg-white p-4 shadow-sm\"\n          >\n            <div className=\"flex justify-between\">\n              <div>\n                <span className=\"text-xs font-semibold\">\n                  {formatDate(comment.createdAt)}\n                </span>\n                {comment.author && (\n                  <span className=\"text-xs font-bold\">\n                    {' '}\n                    by {comment.author.firstName} {comment.author.lastName}\n                  </span>\n                )}\n              </div>\n              {!isPublicView && (\n                <Authorization\n                  policyCheck={POLICIES['comment:delete'](\n                    user.data as User,\n                    comment,\n                  )}\n                >\n                  <DeleteComment discussionId={discussionId} id={comment.id} />\n                </Authorization>\n              )}\n            </div>\n            <MDPreview value={comment.body} />\n          </li>\n        ))}\n      </ul>\n      {commentsQuery.hasNextPage && (\n        <div className=\"flex items-center justify-center py-4\">\n          <Button onClick={() => commentsQuery.fetchNextPage()}>\n            {commentsQuery.isFetchingNextPage ? (\n              <Spinner />\n            ) : (\n              'Load More Comments'\n            )}\n          </Button>\n        </div>\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/features/comments/components/comments.tsx",
    "content": "import { usePathname } from 'next/navigation';\n\nimport { CommentsList } from './comments-list';\nimport { CreateComment } from './create-comment';\n\ntype CommentsProps = {\n  discussionId: string;\n};\n\nexport const Comments = ({ discussionId }: CommentsProps) => {\n  const pathname = usePathname();\n  const isPublicView = pathname?.startsWith?.('/public/');\n  return (\n    <div>\n      <div className=\"mb-4 flex items-center justify-between\">\n        <h3 className=\"text-xl font-bold\">Comments:</h3>\n        {!isPublicView && <CreateComment discussionId={discussionId} />}\n      </div>\n      <CommentsList discussionId={discussionId} />\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/features/comments/components/create-comment.tsx",
    "content": "import { Plus } from 'lucide-react';\n\nimport { Button } from '@/components/ui/button';\nimport { Form, FormDrawer, Textarea } from '@/components/ui/form';\nimport { useNotifications } from '@/components/ui/notifications';\n\nimport {\n  useCreateComment,\n  createCommentInputSchema,\n} from '../api/create-comment';\n\ntype CreateCommentProps = {\n  discussionId: string;\n};\n\nexport const CreateComment = ({ discussionId }: CreateCommentProps) => {\n  const { addNotification } = useNotifications();\n  const createCommentMutation = useCreateComment({\n    discussionId,\n    mutationConfig: {\n      onSuccess: () => {\n        addNotification({\n          type: 'success',\n          title: 'Comment Created',\n        });\n      },\n    },\n  });\n\n  return (\n    <FormDrawer\n      isDone={createCommentMutation.isSuccess}\n      triggerButton={\n        <Button size=\"sm\" icon={<Plus className=\"size-4\" />}>\n          Create Comment\n        </Button>\n      }\n      title=\"Create Comment\"\n      submitButton={\n        <Button\n          isLoading={createCommentMutation.isPending}\n          form=\"create-comment\"\n          type=\"submit\"\n          size=\"sm\"\n          disabled={createCommentMutation.isPending}\n        >\n          Submit\n        </Button>\n      }\n    >\n      <Form\n        id=\"create-comment\"\n        onSubmit={(values) => {\n          createCommentMutation.mutate({\n            data: values,\n          });\n        }}\n        schema={createCommentInputSchema}\n        options={{\n          defaultValues: {\n            body: '',\n            discussionId: discussionId,\n          },\n        }}\n      >\n        {({ register, formState }) => (\n          <Textarea\n            label=\"Body\"\n            error={formState.errors['body']}\n            registration={register('body')}\n          />\n        )}\n      </Form>\n    </FormDrawer>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/features/comments/components/delete-comment.tsx",
    "content": "import { Trash } from 'lucide-react';\n\nimport { Button } from '@/components/ui/button';\nimport { ConfirmationDialog } from '@/components/ui/dialog';\nimport { useNotifications } from '@/components/ui/notifications';\n\nimport { useDeleteComment } from '../api/delete-comment';\n\ntype DeleteCommentProps = {\n  id: string;\n  discussionId: string;\n};\n\nexport const DeleteComment = ({ id, discussionId }: DeleteCommentProps) => {\n  const { addNotification } = useNotifications();\n  const deleteCommentMutation = useDeleteComment({\n    discussionId,\n    mutationConfig: {\n      onSuccess: () => {\n        addNotification({\n          type: 'success',\n          title: 'Comment Deleted',\n        });\n      },\n    },\n  });\n\n  return (\n    <ConfirmationDialog\n      isDone={deleteCommentMutation.isSuccess}\n      icon=\"danger\"\n      title=\"Delete Comment\"\n      body=\"Are you sure you want to delete this comment?\"\n      triggerButton={\n        <Button\n          variant=\"destructive\"\n          size=\"sm\"\n          icon={<Trash className=\"size-4\" />}\n        >\n          Delete Comment\n        </Button>\n      }\n      confirmButton={\n        <Button\n          isLoading={deleteCommentMutation.isPending}\n          type=\"button\"\n          variant=\"destructive\"\n          onClick={() => deleteCommentMutation.mutate({ commentId: id })}\n        >\n          Delete Comment\n        </Button>\n      }\n    />\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/features/discussions/api/create-discussion.ts",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query';\nimport { z } from 'zod';\n\nimport { api } from '@/lib/api-client';\nimport { MutationConfig } from '@/lib/react-query';\nimport { Discussion } from '@/types/api';\n\nimport { getDiscussionsQueryOptions } from './get-discussions';\n\nexport const createDiscussionInputSchema = z.object({\n  title: z.string().min(1, 'Required'),\n  body: z.string().min(1, 'Required'),\n  public: z.boolean(),\n});\n\nexport type CreateDiscussionInput = z.infer<typeof createDiscussionInputSchema>;\n\nexport const createDiscussion = ({\n  data,\n}: {\n  data: CreateDiscussionInput;\n}): Promise<Discussion> => {\n  return api.post(`/discussions`, data);\n};\n\ntype UseCreateDiscussionOptions = {\n  mutationConfig?: MutationConfig<typeof createDiscussion>;\n};\n\nexport const useCreateDiscussion = ({\n  mutationConfig,\n}: UseCreateDiscussionOptions = {}) => {\n  const queryClient = useQueryClient();\n\n  const { onSuccess, ...restConfig } = mutationConfig || {};\n\n  return useMutation({\n    onSuccess: (...args) => {\n      queryClient.invalidateQueries({\n        queryKey: getDiscussionsQueryOptions().queryKey,\n      });\n      onSuccess?.(...args);\n    },\n    ...restConfig,\n    mutationFn: createDiscussion,\n  });\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/features/discussions/api/delete-discussion.ts",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query';\n\nimport { api } from '@/lib/api-client';\nimport { MutationConfig } from '@/lib/react-query';\n\nimport { getDiscussionsQueryOptions } from './get-discussions';\n\nexport const deleteDiscussion = ({\n  discussionId,\n}: {\n  discussionId: string;\n}) => {\n  return api.delete(`/discussions/${discussionId}`);\n};\n\ntype UseDeleteDiscussionOptions = {\n  mutationConfig?: MutationConfig<typeof deleteDiscussion>;\n};\n\nexport const useDeleteDiscussion = ({\n  mutationConfig,\n}: UseDeleteDiscussionOptions = {}) => {\n  const queryClient = useQueryClient();\n\n  const { onSuccess, ...restConfig } = mutationConfig || {};\n\n  return useMutation({\n    onSuccess: (...args) => {\n      queryClient.invalidateQueries({\n        queryKey: getDiscussionsQueryOptions().queryKey,\n      });\n      onSuccess?.(...args);\n    },\n    ...restConfig,\n    mutationFn: deleteDiscussion,\n  });\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/features/discussions/api/get-discussion.ts",
    "content": "import { useQuery, queryOptions } from '@tanstack/react-query';\n\nimport { api, attachCookie } from '@/lib/api-client';\nimport { QueryConfig } from '@/lib/react-query';\nimport { Discussion } from '@/types/api';\n\nexport const getDiscussion = ({\n  discussionId,\n  cookie,\n}: {\n  discussionId: string;\n  cookie?: string;\n}): Promise<{ data: Discussion }> => {\n  return api.get(`/discussions/${discussionId}`, {\n    headers: attachCookie(cookie).headers,\n  });\n};\n\nexport const getDiscussionQueryOptions = (\n  discussionId: string,\n  cookie?: string,\n) => {\n  return queryOptions({\n    queryKey: ['discussions', discussionId],\n    queryFn: () => getDiscussion({ discussionId, cookie }),\n  });\n};\n\ntype UseDiscussionOptions = {\n  discussionId: string;\n  queryConfig?: QueryConfig<typeof getDiscussionQueryOptions>;\n};\n\nexport const useDiscussion = ({\n  discussionId,\n  queryConfig,\n}: UseDiscussionOptions) => {\n  return useQuery({\n    ...getDiscussionQueryOptions(discussionId),\n    ...queryConfig,\n  });\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/features/discussions/api/get-discussions.ts",
    "content": "import { queryOptions, useQuery } from '@tanstack/react-query';\n\nimport { api, attachCookie } from '@/lib/api-client';\nimport { QueryConfig } from '@/lib/react-query';\nimport { Discussion, Meta } from '@/types/api';\n\nexport const getDiscussions = (\n  { page, cookie }: { page?: number; cookie?: string } = { page: 1 },\n): Promise<{\n  data: Discussion[];\n  meta: Meta;\n}> => {\n  return api.get(`/discussions`, {\n    params: {\n      page,\n    },\n    headers: attachCookie(cookie).headers,\n  });\n};\n\nexport const getDiscussionsQueryOptions = ({\n  page,\n  cookie,\n}: { page?: number; cookie?: string } = {}) => {\n  return queryOptions({\n    queryKey: page ? ['discussions', { page }] : ['discussions'],\n    queryFn: () => getDiscussions({ page, cookie }),\n  });\n};\n\ntype UseDiscussionsOptions = {\n  page?: number;\n  queryConfig?: QueryConfig<typeof getDiscussionsQueryOptions>;\n};\n\nexport const useDiscussions = ({\n  queryConfig,\n  page,\n}: UseDiscussionsOptions) => {\n  return useQuery({\n    ...getDiscussionsQueryOptions({ page }),\n    ...queryConfig,\n  });\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/features/discussions/api/update-discussion.ts",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query';\nimport { z } from 'zod';\n\nimport { api } from '@/lib/api-client';\nimport { MutationConfig } from '@/lib/react-query';\nimport { Discussion } from '@/types/api';\n\nimport { getDiscussionQueryOptions } from './get-discussion';\n\nexport const updateDiscussionInputSchema = z.object({\n  title: z.string().min(1, 'Required'),\n  body: z.string().min(1, 'Required'),\n  public: z.boolean(),\n});\n\nexport type UpdateDiscussionInput = z.infer<typeof updateDiscussionInputSchema>;\n\nexport const updateDiscussion = ({\n  data,\n  discussionId,\n}: {\n  data: UpdateDiscussionInput;\n  discussionId: string;\n}): Promise<Discussion> => {\n  return api.patch(`/discussions/${discussionId}`, data);\n};\n\ntype UseUpdateDiscussionOptions = {\n  mutationConfig?: MutationConfig<typeof updateDiscussion>;\n};\n\nexport const useUpdateDiscussion = ({\n  mutationConfig,\n}: UseUpdateDiscussionOptions = {}) => {\n  const queryClient = useQueryClient();\n\n  const { onSuccess, ...restConfig } = mutationConfig || {};\n\n  return useMutation({\n    onSuccess: (data, ...args) => {\n      queryClient.refetchQueries({\n        queryKey: getDiscussionQueryOptions(data.id).queryKey,\n      });\n      onSuccess?.(data, ...args);\n    },\n    ...restConfig,\n    mutationFn: updateDiscussion,\n  });\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/features/discussions/components/create-discussion.tsx",
    "content": "import { Plus } from 'lucide-react';\n\nimport { Button } from '@/components/ui/button';\nimport {\n  Form,\n  FormDrawer,\n  Input,\n  Label,\n  Switch,\n  Textarea,\n} from '@/components/ui/form';\nimport { useNotifications } from '@/components/ui/notifications';\nimport { Authorization, ROLES } from '@/lib/authorization';\n\nimport {\n  createDiscussionInputSchema,\n  useCreateDiscussion,\n} from '../api/create-discussion';\n\nexport const CreateDiscussion = () => {\n  const { addNotification } = useNotifications();\n  const createDiscussionMutation = useCreateDiscussion({\n    mutationConfig: {\n      onSuccess: () => {\n        addNotification({\n          type: 'success',\n          title: 'Discussion Created',\n        });\n      },\n    },\n  });\n\n  return (\n    <Authorization allowedRoles={[ROLES.ADMIN]}>\n      <FormDrawer\n        isDone={createDiscussionMutation.isSuccess}\n        triggerButton={\n          <Button size=\"sm\" icon={<Plus className=\"size-4\" />}>\n            Create Discussion\n          </Button>\n        }\n        title=\"Create Discussion\"\n        submitButton={\n          <Button\n            form=\"create-discussion\"\n            type=\"submit\"\n            size=\"sm\"\n            isLoading={createDiscussionMutation.isPending}\n          >\n            Submit\n          </Button>\n        }\n      >\n        <Form\n          id=\"create-discussion\"\n          onSubmit={(values) => {\n            createDiscussionMutation.mutate({ data: values });\n          }}\n          schema={createDiscussionInputSchema}\n          options={{\n            defaultValues: {\n              title: '',\n              body: '',\n              public: false,\n            },\n          }}\n        >\n          {({ register, formState, setValue, watch }) => (\n            <>\n              <Input\n                label=\"Title\"\n                error={formState.errors['title']}\n                registration={register('title')}\n              />\n\n              <Textarea\n                label=\"Body\"\n                error={formState.errors['body']}\n                registration={register('body')}\n              />\n\n              <div className=\"flex items-center space-x-2\">\n                <Switch\n                  name=\"public\"\n                  onCheckedChange={(value) => setValue('public', value)}\n                  checked={watch('public')}\n                  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`}\n                  id=\"public\"\n                />\n                <Label htmlFor=\"airplane-mode\">Public</Label>\n              </div>\n            </>\n          )}\n        </Form>\n      </FormDrawer>\n    </Authorization>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/features/discussions/components/delete-discussion.tsx",
    "content": "import { Trash } from 'lucide-react';\n\nimport { Button } from '@/components/ui/button';\nimport { ConfirmationDialog } from '@/components/ui/dialog';\nimport { useNotifications } from '@/components/ui/notifications';\nimport { Authorization, ROLES } from '@/lib/authorization';\n\nimport { useDeleteDiscussion } from '../api/delete-discussion';\n\ntype DeleteDiscussionProps = {\n  id: string;\n};\n\nexport const DeleteDiscussion = ({ id }: DeleteDiscussionProps) => {\n  const { addNotification } = useNotifications();\n  const deleteDiscussionMutation = useDeleteDiscussion({\n    mutationConfig: {\n      onSuccess: () => {\n        addNotification({\n          type: 'success',\n          title: 'Discussion Deleted',\n        });\n      },\n    },\n  });\n\n  return (\n    <Authorization allowedRoles={[ROLES.ADMIN]}>\n      <ConfirmationDialog\n        icon=\"danger\"\n        title=\"Delete Discussion\"\n        body=\"Are you sure you want to delete this discussion?\"\n        triggerButton={\n          <Button variant=\"destructive\" icon={<Trash className=\"size-4\" />}>\n            Delete Discussion\n          </Button>\n        }\n        confirmButton={\n          <Button\n            isLoading={deleteDiscussionMutation.isPending}\n            type=\"button\"\n            variant=\"destructive\"\n            onClick={() =>\n              deleteDiscussionMutation.mutate({ discussionId: id })\n            }\n          >\n            Delete Discussion\n          </Button>\n        }\n      />\n    </Authorization>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/features/discussions/components/discussion-view.tsx",
    "content": "import { Link as LinkIcon } from 'lucide-react';\nimport { usePathname } from 'next/navigation';\n\nimport { Link } from '@/components/ui/link';\nimport { MDPreview } from '@/components/ui/md-preview';\nimport { Spinner } from '@/components/ui/spinner';\nimport { paths } from '@/config/paths';\nimport { formatDate } from '@/utils/format';\n\nimport { useDiscussion } from '../api/get-discussion';\nimport { UpdateDiscussion } from '../components/update-discussion';\n\nexport const DiscussionView = ({ discussionId }: { discussionId: string }) => {\n  const pathname = usePathname();\n  const isPublicView = pathname?.startsWith?.('/public/');\n\n  const discussionQuery = useDiscussion({\n    discussionId,\n  });\n\n  if (discussionQuery.isLoading) {\n    return (\n      <div className=\"flex h-48 w-full items-center justify-center\">\n        <Spinner size=\"lg\" />\n      </div>\n    );\n  }\n\n  const discussion = discussionQuery?.data?.data;\n\n  if (!discussion) return null;\n\n  return (\n    <div>\n      <div className=\"flex justify-between\">\n        <span>\n          <span className=\"text-xs font-bold\">\n            {formatDate(discussion.createdAt)}\n          </span>\n          {discussion.author && (\n            <span className=\"ml-2 text-sm font-bold\">\n              by {discussion.author.firstName} {discussion.author.lastName}\n            </span>\n          )}\n        </span>\n        {!isPublicView && discussion.public && (\n          <Link\n            className=\"ml-2 flex items-center gap-2 text-sm font-bold\"\n            href={paths.public.discussion.getHref(discussionId)}\n            target=\"_blank\"\n          >\n            View Public Version <LinkIcon size={16} />\n          </Link>\n        )}\n      </div>\n      <div className=\"mt-6 flex flex-col space-y-16\">\n        {!isPublicView && (\n          <div className=\"flex justify-end\">\n            <UpdateDiscussion discussionId={discussionId} />\n          </div>\n        )}\n        <div>\n          <div className=\"overflow-hidden bg-white shadow sm:rounded-lg\">\n            <div className=\"px-4 py-5 sm:px-6\">\n              <div className=\"mt-1 max-w-2xl text-sm text-gray-500\">\n                <MDPreview value={discussion.body} />\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/features/discussions/components/discussions-list.tsx",
    "content": "import { useQueryClient } from '@tanstack/react-query';\nimport { useRouter } from 'next/router';\n\nimport { Link } from '@/components/ui/link';\nimport { Spinner } from '@/components/ui/spinner';\nimport { Table } from '@/components/ui/table';\nimport { paths } from '@/config/paths';\nimport { formatDate } from '@/utils/format';\n\nimport { getDiscussionQueryOptions } from '../api/get-discussion';\nimport { useDiscussions } from '../api/get-discussions';\n\nimport { DeleteDiscussion } from './delete-discussion';\n\nexport type DiscussionsListProps = {\n  onDiscussionPrefetch?: (id: string) => void;\n};\n\nexport const DiscussionsList = ({\n  onDiscussionPrefetch,\n}: DiscussionsListProps) => {\n  const router = useRouter();\n  const page = router.query.page ? Number(router.query.page) : 1;\n\n  const discussionsQuery = useDiscussions({\n    page: page,\n  });\n  const queryClient = useQueryClient();\n\n  if (discussionsQuery.isLoading) {\n    return (\n      <div className=\"flex h-48 w-full items-center justify-center\">\n        <Spinner size=\"lg\" />\n      </div>\n    );\n  }\n\n  const discussions = discussionsQuery.data?.data;\n  const meta = discussionsQuery.data?.meta;\n\n  if (!discussions) return null;\n\n  return (\n    <Table\n      data={discussions}\n      columns={[\n        {\n          title: 'Title',\n          field: 'title',\n        },\n        {\n          title: 'Created At',\n          field: 'createdAt',\n          Cell({ entry: { createdAt } }) {\n            return <span>{formatDate(createdAt)}</span>;\n          },\n        },\n        {\n          title: '',\n          field: 'id',\n          Cell({ entry: { id } }) {\n            return (\n              <Link\n                onMouseEnter={() => {\n                  // Prefetch the discussion data when the user hovers over the link\n                  queryClient.prefetchQuery(getDiscussionQueryOptions(id));\n                  onDiscussionPrefetch?.(id);\n                }}\n                href={paths.app.discussion.getHref(id)}\n              >\n                View\n              </Link>\n            );\n          },\n        },\n        {\n          title: '',\n          field: 'id',\n          Cell({ entry: { id } }) {\n            return <DeleteDiscussion id={id} />;\n          },\n        },\n      ]}\n      pagination={\n        meta && {\n          totalPages: meta.totalPages,\n          currentPage: meta.page,\n          rootUrl: '',\n        }\n      }\n    />\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/features/discussions/components/update-discussion.tsx",
    "content": "import { Pen } from 'lucide-react';\n\nimport { Button } from '@/components/ui/button';\nimport {\n  Form,\n  FormDrawer,\n  Input,\n  Label,\n  Switch,\n  Textarea,\n} from '@/components/ui/form';\nimport { useNotifications } from '@/components/ui/notifications';\nimport { Authorization, ROLES } from '@/lib/authorization';\n\nimport { useDiscussion } from '../api/get-discussion';\nimport {\n  updateDiscussionInputSchema,\n  useUpdateDiscussion,\n} from '../api/update-discussion';\n\ntype UpdateDiscussionProps = {\n  discussionId: string;\n};\n\nexport const UpdateDiscussion = ({ discussionId }: UpdateDiscussionProps) => {\n  const { addNotification } = useNotifications();\n  const discussionQuery = useDiscussion({ discussionId });\n  const updateDiscussionMutation = useUpdateDiscussion({\n    mutationConfig: {\n      onSuccess: () => {\n        addNotification({\n          type: 'success',\n          title: 'Discussion Updated',\n        });\n      },\n    },\n  });\n\n  const discussion = discussionQuery.data?.data;\n\n  return (\n    <Authorization allowedRoles={[ROLES.ADMIN]}>\n      <FormDrawer\n        isDone={updateDiscussionMutation.isSuccess}\n        triggerButton={\n          <Button icon={<Pen className=\"size-4\" />} size=\"sm\">\n            Update Discussion\n          </Button>\n        }\n        title=\"Update Discussion\"\n        submitButton={\n          <Button\n            form=\"update-discussion\"\n            type=\"submit\"\n            size=\"sm\"\n            isLoading={updateDiscussionMutation.isPending}\n          >\n            Submit\n          </Button>\n        }\n      >\n        <Form\n          id=\"update-discussion\"\n          onSubmit={(values) => {\n            updateDiscussionMutation.mutate({\n              data: values,\n              discussionId,\n            });\n          }}\n          options={{\n            defaultValues: {\n              title: discussion?.title ?? '',\n              body: discussion?.body ?? '',\n              public: discussion?.public ?? false,\n            },\n          }}\n          schema={updateDiscussionInputSchema}\n        >\n          {({ register, formState, setValue, watch }) => (\n            <>\n              <Input\n                label=\"Title\"\n                error={formState.errors['title']}\n                registration={register('title')}\n              />\n              <Textarea\n                label=\"Body\"\n                error={formState.errors['body']}\n                registration={register('body')}\n              />\n\n              <div className=\"flex items-center space-x-2\">\n                <Switch\n                  name=\"public\"\n                  onCheckedChange={(value) => setValue('public', value)}\n                  checked={watch('public')}\n                  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`}\n                  id=\"public\"\n                />\n                <Label htmlFor=\"airplane-mode\">Public</Label>\n              </div>\n            </>\n          )}\n        </Form>\n      </FormDrawer>\n    </Authorization>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/features/teams/api/get-teams.ts",
    "content": "import { queryOptions, useQuery } from '@tanstack/react-query';\n\nimport { api } from '@/lib/api-client';\nimport { QueryConfig } from '@/lib/react-query';\nimport { Team } from '@/types/api';\n\nexport const getTeams = (): Promise<{ data: Team[] }> => {\n  return api.get('/teams');\n};\n\nexport const getTeamsQueryOptions = () => {\n  return queryOptions({\n    queryKey: ['teams'],\n    queryFn: () => getTeams(),\n  });\n};\n\ntype UseTeamsOptions = {\n  queryConfig?: QueryConfig<typeof getTeamsQueryOptions>;\n};\n\nexport const useTeams = ({ queryConfig = {} }: UseTeamsOptions = {}) => {\n  return useQuery({\n    ...getTeamsQueryOptions(),\n    ...queryConfig,\n  });\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/features/users/api/delete-user.ts",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query';\n\nimport { api } from '@/lib/api-client';\nimport { MutationConfig } from '@/lib/react-query';\n\nimport { getUsersQueryOptions } from './get-users';\n\nexport type DeleteUserDTO = {\n  userId: string;\n};\n\nexport const deleteUser = ({ userId }: DeleteUserDTO) => {\n  return api.delete(`/users/${userId}`);\n};\n\ntype UseDeleteUserOptions = {\n  mutationConfig?: MutationConfig<typeof deleteUser>;\n};\n\nexport const useDeleteUser = ({\n  mutationConfig,\n}: UseDeleteUserOptions = {}) => {\n  const queryClient = useQueryClient();\n\n  const { onSuccess, ...restConfig } = mutationConfig || {};\n\n  return useMutation({\n    onSuccess: (...args) => {\n      queryClient.invalidateQueries({\n        queryKey: getUsersQueryOptions().queryKey,\n      });\n      onSuccess?.(...args);\n    },\n    ...restConfig,\n    mutationFn: deleteUser,\n  });\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/features/users/api/get-users.ts",
    "content": "import { queryOptions, useQuery } from '@tanstack/react-query';\n\nimport { api } from '@/lib/api-client';\nimport { QueryConfig } from '@/lib/react-query';\nimport { User } from '@/types/api';\n\nexport const getUsers = (): Promise<{ data: User[] }> => {\n  return api.get(`/users`);\n};\n\nexport const getUsersQueryOptions = () => {\n  return queryOptions({\n    queryKey: ['users'],\n    queryFn: getUsers,\n  });\n};\n\ntype UseUsersOptions = {\n  queryConfig?: QueryConfig<typeof getUsersQueryOptions>;\n};\n\nexport const useUsers = ({ queryConfig }: UseUsersOptions = {}) => {\n  return useQuery({\n    ...getUsersQueryOptions(),\n    ...queryConfig,\n  });\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/features/users/api/update-profile.ts",
    "content": "import { useMutation } from '@tanstack/react-query';\nimport { z } from 'zod';\n\nimport { api } from '@/lib/api-client';\nimport { useUser } from '@/lib/auth';\nimport { MutationConfig } from '@/lib/react-query';\n\nexport const updateProfileInputSchema = z.object({\n  email: z.string().min(1, 'Required').email('Invalid email'),\n  firstName: z.string().min(1, 'Required'),\n  lastName: z.string().min(1, 'Required'),\n  bio: z.string(),\n});\n\nexport type UpdateProfileInput = z.infer<typeof updateProfileInputSchema>;\n\nexport const updateProfile = ({ data }: { data: UpdateProfileInput }) => {\n  return api.patch(`/users/profile`, data);\n};\n\ntype UseUpdateProfileOptions = {\n  mutationConfig?: MutationConfig<typeof updateProfile>;\n};\n\nexport const useUpdateProfile = ({\n  mutationConfig,\n}: UseUpdateProfileOptions = {}) => {\n  const { refetch: refetchUser } = useUser();\n\n  const { onSuccess, ...restConfig } = mutationConfig || {};\n\n  return useMutation({\n    onSuccess: (...args) => {\n      refetchUser();\n      onSuccess?.(...args);\n    },\n    ...restConfig,\n    mutationFn: updateProfile,\n  });\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/features/users/components/delete-user.tsx",
    "content": "import { Button } from '@/components/ui/button';\nimport { ConfirmationDialog } from '@/components/ui/dialog';\nimport { useNotifications } from '@/components/ui/notifications';\nimport { useUser } from '@/lib/auth';\n\nimport { useDeleteUser } from '../api/delete-user';\n\ntype DeleteUserProps = {\n  id: string;\n};\n\nexport const DeleteUser = ({ id }: DeleteUserProps) => {\n  const user = useUser();\n  const { addNotification } = useNotifications();\n  const deleteUserMutation = useDeleteUser({\n    mutationConfig: {\n      onSuccess: () => {\n        addNotification({\n          type: 'success',\n          title: 'User Deleted',\n        });\n      },\n    },\n  });\n\n  if (user.data?.id === id) return null;\n\n  return (\n    <ConfirmationDialog\n      icon=\"danger\"\n      title=\"Delete User\"\n      body=\"Are you sure you want to delete this user?\"\n      triggerButton={<Button variant=\"destructive\">Delete</Button>}\n      confirmButton={\n        <Button\n          isLoading={deleteUserMutation.isPending}\n          type=\"button\"\n          variant=\"destructive\"\n          onClick={() => deleteUserMutation.mutate({ userId: id })}\n        >\n          Delete User\n        </Button>\n      }\n    />\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/features/users/components/update-profile.tsx",
    "content": "import { Pen } from 'lucide-react';\n\nimport { Button } from '@/components/ui/button';\nimport { Form, FormDrawer, Input, Textarea } from '@/components/ui/form';\nimport { useNotifications } from '@/components/ui/notifications';\nimport { useUser } from '@/lib/auth';\n\nimport {\n  updateProfileInputSchema,\n  useUpdateProfile,\n} from '../api/update-profile';\n\nexport const UpdateProfile = () => {\n  const user = useUser();\n  const { addNotification } = useNotifications();\n  const updateProfileMutation = useUpdateProfile({\n    mutationConfig: {\n      onSuccess: () => {\n        addNotification({\n          type: 'success',\n          title: 'Profile Updated',\n        });\n      },\n    },\n  });\n\n  return (\n    <FormDrawer\n      isDone={updateProfileMutation.isSuccess}\n      triggerButton={\n        <Button icon={<Pen className=\"size-4\" />} size=\"sm\">\n          Update Profile\n        </Button>\n      }\n      title=\"Update Profile\"\n      submitButton={\n        <Button\n          form=\"update-profile\"\n          type=\"submit\"\n          size=\"sm\"\n          isLoading={updateProfileMutation.isPending}\n        >\n          Submit\n        </Button>\n      }\n    >\n      <Form\n        id=\"update-profile\"\n        onSubmit={(values) => {\n          updateProfileMutation.mutate({ data: values });\n        }}\n        options={{\n          defaultValues: {\n            firstName: user.data?.firstName ?? '',\n            lastName: user.data?.lastName ?? '',\n            email: user.data?.email ?? '',\n            bio: user.data?.bio ?? '',\n          },\n        }}\n        schema={updateProfileInputSchema}\n      >\n        {({ register, formState }) => (\n          <>\n            <Input\n              label=\"First Name\"\n              error={formState.errors['firstName']}\n              registration={register('firstName')}\n            />\n            <Input\n              label=\"Last Name\"\n              error={formState.errors['lastName']}\n              registration={register('lastName')}\n            />\n            <Input\n              label=\"Email Address\"\n              type=\"email\"\n              error={formState.errors['email']}\n              registration={register('email')}\n            />\n\n            <Textarea\n              label=\"Bio\"\n              error={formState.errors['bio']}\n              registration={register('bio')}\n            />\n          </>\n        )}\n      </Form>\n    </FormDrawer>\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/features/users/components/users-list.tsx",
    "content": "import { Spinner } from '@/components/ui/spinner';\nimport { Table } from '@/components/ui/table';\nimport { formatDate } from '@/utils/format';\n\nimport { useUsers } from '../api/get-users';\n\nimport { DeleteUser } from './delete-user';\n\nexport const UsersList = () => {\n  const usersQuery = useUsers();\n\n  if (usersQuery.isLoading) {\n    return (\n      <div className=\"flex h-48 w-full items-center justify-center\">\n        <Spinner size=\"lg\" />\n      </div>\n    );\n  }\n\n  const users = usersQuery.data?.data;\n\n  if (!users) return null;\n\n  return (\n    <Table\n      data={users}\n      columns={[\n        {\n          title: 'First Name',\n          field: 'firstName',\n        },\n        {\n          title: 'Last Name',\n          field: 'lastName',\n        },\n        {\n          title: 'Email',\n          field: 'email',\n        },\n        {\n          title: 'Role',\n          field: 'role',\n        },\n        {\n          title: 'Created At',\n          field: 'createdAt',\n          Cell({ entry: { createdAt } }) {\n            return <span>{formatDate(createdAt)}</span>;\n          },\n        },\n        {\n          title: '',\n          field: 'id',\n          Cell({ entry: { id } }) {\n            return <DeleteUser id={id} />;\n          },\n        },\n      ]}\n    />\n  );\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/hooks/__tests__/use-disclosure.test.ts",
    "content": "import { renderHook, act } from '@testing-library/react';\n\nimport { useDisclosure } from '../use-disclosure';\n\ntest('should open the state', () => {\n  const { result } = renderHook(() => useDisclosure());\n\n  expect(result.current.isOpen).toBe(false);\n\n  act(() => {\n    result.current.open();\n  });\n\n  expect(result.current.isOpen).toBe(true);\n});\n\ntest('should close the state', () => {\n  const { result } = renderHook(() => useDisclosure());\n\n  expect(result.current.isOpen).toBe(false);\n\n  act(() => {\n    result.current.close();\n  });\n\n  expect(result.current.isOpen).toBe(false);\n});\n\ntest('should toggle the state', () => {\n  const { result } = renderHook(() => useDisclosure());\n\n  expect(result.current.isOpen).toBe(false);\n\n  act(() => {\n    result.current.toggle();\n  });\n\n  expect(result.current.isOpen).toBe(true);\n\n  act(() => {\n    result.current.toggle();\n  });\n\n  expect(result.current.isOpen).toBe(false);\n});\n\ntest('should define initial state', () => {\n  const { result } = renderHook(() => useDisclosure(true));\n\n  expect(result.current.isOpen).toBe(true);\n\n  act(() => {\n    result.current.toggle();\n  });\n\n  expect(result.current.isOpen).toBe(false);\n});\n"
  },
  {
    "path": "apps/nextjs-pages/src/hooks/use-disclosure.ts",
    "content": "import * as React from 'react';\n\nexport const useDisclosure = (initial = false) => {\n  const [isOpen, setIsOpen] = React.useState(initial);\n\n  const open = React.useCallback(() => setIsOpen(true), []);\n  const close = React.useCallback(() => setIsOpen(false), []);\n  const toggle = React.useCallback(() => setIsOpen((state) => !state), []);\n\n  return { isOpen, open, close, toggle };\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/lib/__tests__/authorization.test.tsx",
    "content": "import { createUser, renderApp, screen } from '@/testing/test-utils';\n\nimport { Authorization, ROLES } from '../authorization';\n\ntest('should view protected resource if user role is matching', async () => {\n  const user = await createUser({\n    role: ROLES.ADMIN,\n  });\n\n  const protectedResource = 'This is very confidential data';\n\n  await renderApp(\n    <Authorization allowedRoles={[ROLES.ADMIN]}>\n      {protectedResource}\n    </Authorization>,\n    {\n      user,\n    },\n  );\n\n  expect(screen.getByText(protectedResource)).toBeInTheDocument();\n});\n\ntest('should not view protected resource if user role does not match and show fallback message instead', async () => {\n  const user = await createUser({\n    role: ROLES.USER,\n  });\n\n  const protectedResource = 'This is very confidential data';\n\n  const forbiddenMessage = 'You are unauthorized to view this resource';\n  await renderApp(\n    <Authorization\n      forbiddenFallback={<div>{forbiddenMessage}</div>}\n      allowedRoles={[ROLES.ADMIN]}\n    >\n      {protectedResource}\n    </Authorization>,\n    { user },\n  );\n\n  await screen.findByText(forbiddenMessage);\n\n  expect(screen.queryByText(protectedResource)).not.toBeInTheDocument();\n\n  expect(screen.getByText(forbiddenMessage)).toBeInTheDocument();\n});\n\ntest('should view protected resource if policy check passes', async () => {\n  const user = await createUser({\n    role: ROLES.ADMIN,\n  });\n\n  const protectedResource = 'This is very confidential data';\n\n  await renderApp(\n    <Authorization policyCheck={true}>{protectedResource}</Authorization>,\n    { user },\n  );\n\n  expect(screen.getByText(protectedResource)).toBeInTheDocument();\n});\n\ntest('should not view protected resource if policy check fails and show fallback message instead', async () => {\n  const user = await createUser({\n    role: ROLES.USER,\n  });\n\n  const protectedResource = 'This is very confidential data';\n\n  const forbiddenMessage = 'You are unauthorized to view this resource';\n  await renderApp(\n    <Authorization\n      forbiddenFallback={<div>{forbiddenMessage}</div>}\n      policyCheck={false}\n    >\n      {protectedResource}\n    </Authorization>,\n    { user },\n  );\n\n  expect(screen.queryByText(protectedResource)).not.toBeInTheDocument();\n\n  expect(screen.getByText(forbiddenMessage)).toBeInTheDocument();\n});\n"
  },
  {
    "path": "apps/nextjs-pages/src/lib/api-client.ts",
    "content": "import Axios, { InternalAxiosRequestConfig } from 'axios';\n\nimport { useNotifications } from '@/components/ui/notifications';\nimport { env } from '@/config/env';\nimport { paths } from '@/config/paths';\n\nfunction authRequestInterceptor(config: InternalAxiosRequestConfig) {\n  if (config.headers) {\n    config.headers.Accept = 'application/json';\n  }\n\n  config.withCredentials = true;\n  return config;\n}\n\nexport const api = Axios.create({\n  baseURL: env.API_URL,\n});\n\napi.interceptors.request.use(authRequestInterceptor);\napi.interceptors.response.use(\n  (response) => {\n    return response.data;\n  },\n  (error) => {\n    const message = error.response?.data?.message || error.message;\n    useNotifications.getState().addNotification({\n      type: 'error',\n      title: 'Error',\n      message,\n    });\n\n    if (error.response?.status === 401) {\n      if (typeof window !== 'undefined') {\n        const searchParams = new URLSearchParams();\n        const redirectTo =\n          searchParams.get('redirectTo') || window.location.pathname;\n        window.location.href = paths.auth.login.getHref(redirectTo);\n      }\n    }\n\n    return Promise.reject(error);\n  },\n);\n\n// if the endpoint requires the visiting user to be authenticated,\n// attaching cookies is required for requests made on the server side\nexport const attachCookie = (\n  cookie?: string,\n  headers?: Record<string, string>,\n) => {\n  return {\n    headers: {\n      ...headers,\n      ...(cookie ? { Cookie: cookie } : {}),\n    },\n  };\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/lib/auth.tsx",
    "content": "import { useRouter } from 'next/router';\nimport { useEffect } from 'react';\nimport { configureAuth } from 'react-query-auth';\nimport { z } from 'zod';\n\nimport { paths } from '@/config/paths';\nimport { AuthResponse, User } from '@/types/api';\n\nimport { api } from './api-client';\n\n// api call definitions for auth (types, schemas, requests):\n// these are not part of features as this is a module shared across features\n\nconst getUser = async (): Promise<User> => {\n  const response = await api.get('/auth/me');\n\n  return response.data;\n};\n\nconst logout = (): Promise<void> => {\n  return api.post('/auth/logout');\n};\n\nexport const loginInputSchema = z.object({\n  email: z.string().min(1, 'Required').email('Invalid email'),\n  password: z.string().min(5, 'Required'),\n});\n\nexport type LoginInput = z.infer<typeof loginInputSchema>;\nconst loginWithEmailAndPassword = (data: LoginInput): Promise<AuthResponse> => {\n  return api.post('/auth/login', data);\n};\n\nexport const registerInputSchema = z\n  .object({\n    email: z.string().min(1, 'Required'),\n    firstName: z.string().min(1, 'Required'),\n    lastName: z.string().min(1, 'Required'),\n    password: z.string().min(5, 'Required'),\n  })\n  .and(\n    z\n      .object({\n        teamId: z.string().min(1, 'Required'),\n        teamName: z.null().default(null),\n      })\n      .or(\n        z.object({\n          teamName: z.string().min(1, 'Required'),\n          teamId: z.null().default(null),\n        }),\n      ),\n  );\n\nexport type RegisterInput = z.infer<typeof registerInputSchema>;\n\nconst registerWithEmailAndPassword = (\n  data: RegisterInput,\n): Promise<AuthResponse> => {\n  return api.post('/auth/register', data);\n};\n\nconst authConfig = {\n  userFn: getUser,\n  loginFn: async (data: LoginInput) => {\n    const response = await loginWithEmailAndPassword(data);\n    return response.user;\n  },\n  registerFn: async (data: RegisterInput) => {\n    const response = await registerWithEmailAndPassword(data);\n    return response.user;\n  },\n  logoutFn: logout,\n};\n\nexport const { useUser, useLogin, useLogout, useRegister, AuthLoader } =\n  configureAuth(authConfig);\n\nexport const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {\n  const user = useUser();\n  const router = useRouter();\n\n  useEffect(() => {\n    if (!user.data) {\n      router.replace(paths.auth.login.getHref(router.pathname));\n    }\n  }, [user.data, router]);\n\n  return children;\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/lib/authorization.tsx",
    "content": "import { useRouter } from 'next/router';\nimport * as React from 'react';\n\nimport { Comment, User } from '@/types/api';\n\nimport { useUser } from './auth';\n\nexport enum ROLES {\n  ADMIN = 'ADMIN',\n  USER = 'USER',\n}\n\ntype RoleTypes = keyof typeof ROLES;\n\nexport const POLICIES = {\n  'comment:delete': (user: User, comment: Comment) => {\n    if (user?.role === 'ADMIN') {\n      return true;\n    }\n\n    if (user?.role === 'USER' && comment.author?.id === user.id) {\n      return true;\n    }\n\n    return false;\n  },\n};\n\nexport const useAuthorization = () => {\n  const user = useUser();\n  const router = useRouter();\n\n  if (!user.data && !user.isLoading) {\n    const redirectTo = encodeURIComponent(router.pathname);\n    window.location.href = `/auth/login?redirectTo=${redirectTo}`;\n  }\n\n  const checkAccess = React.useCallback(\n    ({ allowedRoles }: { allowedRoles: RoleTypes[] }) => {\n      if (allowedRoles && allowedRoles.length > 0 && user.data) {\n        return allowedRoles?.includes(user.data.role);\n      }\n\n      return true;\n    },\n    [user.data],\n  );\n\n  return { checkAccess, role: user?.data?.role };\n};\n\ntype AuthorizationProps = {\n  forbiddenFallback?: React.ReactNode;\n  children: React.ReactNode;\n} & (\n  | {\n      allowedRoles: RoleTypes[];\n      policyCheck?: never;\n    }\n  | {\n      allowedRoles?: never;\n      policyCheck: boolean;\n    }\n);\n\nexport const Authorization = ({\n  policyCheck,\n  allowedRoles,\n  forbiddenFallback = null,\n  children,\n}: AuthorizationProps) => {\n  const { checkAccess } = useAuthorization();\n\n  let canAccess = false;\n\n  if (allowedRoles) {\n    canAccess = checkAccess({ allowedRoles });\n  }\n\n  if (typeof policyCheck !== 'undefined') {\n    canAccess = policyCheck;\n  }\n\n  return <>{canAccess ? children : forbiddenFallback}</>;\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/lib/react-query.ts",
    "content": "import { UseMutationOptions, DefaultOptions } from '@tanstack/react-query';\n\nexport const queryConfig = {\n  queries: {\n    // throwOnError: true,\n    refetchOnWindowFocus: false,\n    retry: false,\n    staleTime: 1000 * 60,\n  },\n} satisfies DefaultOptions;\n\nexport type ApiFnReturnType<FnType extends (...args: any) => Promise<any>> =\n  Awaited<ReturnType<FnType>>;\n\nexport type QueryConfig<T extends (...args: any[]) => any> = Omit<\n  ReturnType<T>,\n  'queryKey' | 'queryFn'\n>;\n\nexport type MutationConfig<\n  MutationFnType extends (...args: any) => Promise<any>,\n> = UseMutationOptions<\n  ApiFnReturnType<MutationFnType>,\n  Error,\n  Parameters<MutationFnType>[0]\n>;\n"
  },
  {
    "path": "apps/nextjs-pages/src/pages/404.tsx",
    "content": "import { Link } from '@/components/ui/link';\nimport { paths } from '@/config/paths';\n\nconst NotFoundPage = () => {\n  return (\n    <div className=\"mt-52 flex flex-col items-center font-semibold\">\n      <h1>404 - Not Found</h1>\n      <p>Sorry, the page you are looking for does not exist.</p>\n      <Link href={paths.home.getHref()} replace>\n        Go to Home\n      </Link>\n    </div>\n  );\n};\n\nexport default NotFoundPage;\n"
  },
  {
    "path": "apps/nextjs-pages/src/pages/_app.tsx",
    "content": "import { NextPage } from 'next';\nimport type { AppProps } from 'next/app';\nimport { ReactElement, ReactNode } from 'react';\n\nimport { AppProvider } from '@/app/provider';\n\nimport '@/styles/globals.css';\n\n// eslint-disable-next-line @typescript-eslint/ban-types\nexport type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {\n  getLayout?: (page: ReactElement) => ReactNode;\n};\n\ntype AppPropsWithLayout = AppProps & {\n  Component: NextPageWithLayout;\n};\n\nexport default function App({ Component, pageProps }: AppPropsWithLayout) {\n  const getLayout = Component.getLayout ?? ((page) => page);\n  return <AppProvider>{getLayout(<Component {...pageProps} />)}</AppProvider>;\n}\n"
  },
  {
    "path": "apps/nextjs-pages/src/pages/app/discussions/[discussionId].tsx",
    "content": "export { DiscussionPage as default } from '@/app/pages/app/discussions/discussion';\n"
  },
  {
    "path": "apps/nextjs-pages/src/pages/app/discussions/index.tsx",
    "content": "export { DiscussionsPage as default } from '@/app/pages/app/discussions/discussions';\n"
  },
  {
    "path": "apps/nextjs-pages/src/pages/app/index.tsx",
    "content": "export { DashboardPage as default } from '@/app/pages/app/dashboard';\n"
  },
  {
    "path": "apps/nextjs-pages/src/pages/app/profile.tsx",
    "content": "export { ProfilePage as default } from '@/app/pages/app/profile';\n"
  },
  {
    "path": "apps/nextjs-pages/src/pages/app/users.tsx",
    "content": "export { UsersPage as default } from '@/app/pages/app/users';\n"
  },
  {
    "path": "apps/nextjs-pages/src/pages/auth/login.tsx",
    "content": "export { LoginPage as default } from '@/app/pages/auth/login';\n"
  },
  {
    "path": "apps/nextjs-pages/src/pages/auth/register.tsx",
    "content": "export { RegisterPage as default } from '@/app/pages/auth/register';\n"
  },
  {
    "path": "apps/nextjs-pages/src/pages/index.tsx",
    "content": "import { useRouter } from 'next/router';\n\nimport { Head } from '@/components/seo';\nimport { Button } from '@/components/ui/button';\nimport { paths } from '@/config/paths';\nimport { useUser } from '@/lib/auth';\n\nexport const HomePage = () => {\n  const router = useRouter();\n  const user = useUser();\n\n  const handleStart = () => {\n    if (user.data) {\n      router.push(paths.app.dashboard.getHref());\n    } else {\n      router.push(paths.auth.login.getHref());\n    }\n  };\n\n  return (\n    <>\n      <Head\n        title={'Bulletproof React'}\n        description=\"Welcome to bulletproof react\"\n      />\n      <div className=\"flex h-screen items-center bg-white\">\n        <div className=\"mx-auto max-w-7xl px-4 py-12 text-center sm:px-6 lg:px-8 lg:py-16\">\n          <h2 className=\"text-3xl font-extrabold tracking-tight text-gray-900 sm:text-4xl\">\n            <span className=\"block\">Bulletproof React</span>\n          </h2>\n          <img src=\"/logo.svg\" alt=\"react\" />\n          <p>Showcasing Best Practices For Building React Applications</p>\n          <div className=\"mt-8 flex justify-center\">\n            <div className=\"inline-flex rounded-md shadow\">\n              <Button\n                onClick={handleStart}\n                icon={\n                  <svg\n                    xmlns=\"http://www.w3.org/2000/svg\"\n                    className=\"size-6\"\n                    fill=\"none\"\n                    viewBox=\"0 0 24 24\"\n                    stroke=\"currentColor\"\n                  >\n                    <path\n                      strokeLinecap=\"round\"\n                      strokeLinejoin=\"round\"\n                      strokeWidth=\"2\"\n                      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\"\n                    />\n                  </svg>\n                }\n              >\n                Get started\n              </Button>\n            </div>\n            <div className=\"ml-3 inline-flex\">\n              <a\n                href=\"https://github.com/alan2207/bulletproof-react\"\n                target=\"_blank\"\n                rel=\"noreferrer\"\n              >\n                <Button\n                  variant=\"outline\"\n                  icon={\n                    <svg\n                      fill=\"currentColor\"\n                      viewBox=\"0 0 24 24\"\n                      className=\"size-6\"\n                    >\n                      <path\n                        fillRule=\"evenodd\"\n                        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\"\n                        clipRule=\"evenodd\"\n                      />\n                    </svg>\n                  }\n                >\n                  Github Repo\n                </Button>\n              </a>\n            </div>\n          </div>\n        </div>\n      </div>\n    </>\n  );\n};\n\nexport default HomePage;\n"
  },
  {
    "path": "apps/nextjs-pages/src/pages/public/discussions/[discussionId].tsx",
    "content": "export {\n  getServerSideProps,\n  PublicDiscussionPage as default,\n} from '@/app/pages/app/discussions/discussion';\n"
  },
  {
    "path": "apps/nextjs-pages/src/styles/globals.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer base {\n  :root {\n    --background: 0 0% 100%;\n    --foreground: 222.2 84% 4.9%;\n\n    --card: 0 0% 100%;\n    --card-foreground: 222.2 84% 4.9%;\n\n    --popover: 0 0% 100%;\n    --popover-foreground: 222.2 84% 4.9%;\n\n    --primary: 222.2 47.4% 11.2%;\n    --primary-foreground: 210 40% 98%;\n\n    --secondary: 210 40% 96.1%;\n    --secondary-foreground: 222.2 47.4% 11.2%;\n\n    --muted: 210 40% 96.1%;\n    --muted-foreground: 215.4 16.3% 46.9%;\n\n    --accent: 210 40% 96.1%;\n    --accent-foreground: 222.2 47.4% 11.2%;\n\n    --destructive: 0 84.2% 60.2%;\n    --destructive-foreground: 210 40% 98%;\n\n    --border: 214.3 31.8% 91.4%;\n    --input: 214.3 31.8% 91.4%;\n    --ring: 222.2 84% 4.9%;\n\n    --radius: 0.5rem;\n  }\n\n  .dark {\n    --background: 222.2 84% 4.9%;\n    --foreground: 210 40% 98%;\n\n    --card: 222.2 84% 4.9%;\n    --card-foreground: 210 40% 98%;\n\n    --popover: 222.2 84% 4.9%;\n    --popover-foreground: 210 40% 98%;\n\n    --primary: 210 40% 98%;\n    --primary-foreground: 222.2 47.4% 11.2%;\n\n    --secondary: 217.2 32.6% 17.5%;\n    --secondary-foreground: 210 40% 98%;\n\n    --muted: 217.2 32.6% 17.5%;\n    --muted-foreground: 215 20.2% 65.1%;\n\n    --accent: 217.2 32.6% 17.5%;\n    --accent-foreground: 210 40% 98%;\n\n    --destructive: 0 62.8% 30.6%;\n    --destructive-foreground: 210 40% 98%;\n\n    --border: 217.2 32.6% 17.5%;\n    --input: 217.2 32.6% 17.5%;\n    --ring: 212.7 26.8% 83.9%;\n  }\n}\n\n@layer base {\n  * {\n    @apply border-border;\n  }\n  body {\n    @apply bg-background text-foreground;\n  }\n}\n\nbody {\n  margin: 0;\n  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',\n    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',\n    sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',\n    monospace;\n}\n"
  },
  {
    "path": "apps/nextjs-pages/src/testing/data-generators.ts",
    "content": "import {\n  randCompanyName,\n  randUserName,\n  randEmail,\n  randParagraph,\n  randUuid,\n  randPassword,\n  randCatchPhrase,\n} from '@ngneat/falso';\n\nconst generateUser = () => ({\n  id: randUuid() + Math.random(),\n  firstName: randUserName({ withAccents: false }),\n  lastName: randUserName({ withAccents: false }),\n  email: randEmail(),\n  password: randPassword(),\n  teamId: randUuid(),\n  teamName: randCompanyName(),\n  role: 'ADMIN',\n  bio: randParagraph(),\n  createdAt: Date.now(),\n});\n\nexport const createUser = <T extends Partial<ReturnType<typeof generateUser>>>(\n  overrides?: T,\n) => {\n  return { ...generateUser(), ...overrides };\n};\n\nconst generateTeam = () => ({\n  id: randUuid(),\n  name: randCompanyName(),\n  description: randParagraph(),\n  createdAt: Date.now(),\n});\n\nexport const createTeam = <T extends Partial<ReturnType<typeof generateTeam>>>(\n  overrides?: T,\n) => {\n  return { ...generateTeam(), ...overrides };\n};\n\nconst generateDiscussion = () => ({\n  id: randUuid(),\n  title: randCatchPhrase(),\n  body: randParagraph(),\n  createdAt: Date.now(),\n  public: true,\n});\n\nexport const createDiscussion = <\n  T extends Partial<ReturnType<typeof generateDiscussion>>,\n>(\n  overrides?: T & {\n    authorId?: string;\n    teamId?: string;\n  },\n) => {\n  return { ...generateDiscussion(), ...overrides };\n};\n\nconst generateComment = () => ({\n  id: randUuid(),\n  body: randParagraph(),\n  createdAt: Date.now(),\n});\n\nexport const createComment = <\n  T extends Partial<ReturnType<typeof generateComment>>,\n>(\n  overrides?: T & {\n    authorId?: string;\n    discussionId?: string;\n  },\n) => {\n  return { ...generateComment(), ...overrides };\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/testing/mocks/browser.ts",
    "content": "import { setupWorker } from 'msw/browser';\n\nimport { handlers } from './handlers';\n\nexport const worker = setupWorker(...handlers);\n"
  },
  {
    "path": "apps/nextjs-pages/src/testing/mocks/db.ts",
    "content": "import { factory, primaryKey } from '@mswjs/data';\nimport { nanoid } from 'nanoid';\n\nconst models = {\n  user: {\n    id: primaryKey(nanoid),\n    firstName: String,\n    lastName: String,\n    email: String,\n    password: String,\n    teamId: String,\n    role: String,\n    bio: String,\n    createdAt: Date.now,\n  },\n  team: {\n    id: primaryKey(nanoid),\n    name: String,\n    description: String,\n    createdAt: Date.now,\n  },\n  discussion: {\n    id: primaryKey(nanoid),\n    title: String,\n    body: String,\n    authorId: String,\n    teamId: String,\n    createdAt: Date.now,\n    public: Boolean,\n  },\n  comment: {\n    id: primaryKey(nanoid),\n    body: String,\n    authorId: String,\n    discussionId: String,\n    createdAt: Date.now,\n  },\n};\n\nexport const db = factory(models);\n\nexport type Model = keyof typeof models;\n\nconst dbFilePath = 'mocked-db.json';\n\nexport const loadDb = async () => {\n  // If we are running in a Node.js environment\n  if (typeof window === 'undefined') {\n    const { readFile, writeFile } = await import('fs/promises');\n    try {\n      const data = await readFile(dbFilePath, 'utf8');\n      return JSON.parse(data);\n    } catch (error: any) {\n      if (error?.code === 'ENOENT') {\n        const emptyDB = {};\n        await writeFile(dbFilePath, JSON.stringify(emptyDB, null, 2));\n        return emptyDB;\n      } else {\n        console.error('Error loading mocked DB:', error);\n        return null;\n      }\n    }\n  }\n  // If we are running in a browser environment\n  return Object.assign(\n    JSON.parse(window.localStorage.getItem('msw-db') || '{}'),\n  );\n};\n\nexport const storeDb = async (data: string) => {\n  // If we are running in a Node.js environment\n  if (typeof window === 'undefined') {\n    const { writeFile } = await import('fs/promises');\n    await writeFile(dbFilePath, data);\n  } else {\n    // If we are running in a browser environment\n    window.localStorage.setItem('msw-db', data);\n  }\n};\n\nexport const persistDb = async (model: Model) => {\n  if (process.env.NODE_ENV === 'test') return;\n  const data = await loadDb();\n  data[model] = db[model].getAll();\n  await storeDb(JSON.stringify(data));\n};\n\nexport const initializeDb = async () => {\n  const database = await loadDb();\n  Object.entries(db).forEach(([key, model]) => {\n    const dataEntres = database[key];\n    if (dataEntres) {\n      dataEntres?.forEach((entry: Record<string, any>) => {\n        model.create(entry);\n      });\n    }\n  });\n};\n\nexport const resetDb = () => {\n  window.localStorage.clear();\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/testing/mocks/handlers/auth.ts",
    "content": "import Cookies from 'js-cookie';\nimport { HttpResponse, http } from 'msw';\n\nimport { env } from '@/config/env';\n\nimport { db, persistDb } from '../db';\nimport {\n  authenticate,\n  hash,\n  requireAuth,\n  AUTH_COOKIE,\n  networkDelay,\n} from '../utils';\n\ntype RegisterBody = {\n  firstName: string;\n  lastName: string;\n  email: string;\n  password: string;\n  teamId?: string;\n  teamName?: string;\n};\n\ntype LoginBody = {\n  email: string;\n  password: string;\n};\n\nexport const authHandlers = [\n  http.post(`${env.API_URL}/auth/register`, async ({ request }) => {\n    await networkDelay();\n    try {\n      const userObject = (await request.json()) as RegisterBody;\n\n      const existingUser = db.user.findFirst({\n        where: {\n          email: {\n            equals: userObject.email,\n          },\n        },\n      });\n\n      if (existingUser) {\n        return HttpResponse.json(\n          { message: 'The user already exists' },\n          { status: 400 },\n        );\n      }\n\n      let teamId;\n      let role;\n\n      if (!userObject.teamId) {\n        const team = db.team.create({\n          name: userObject.teamName ?? `${userObject.firstName} Team`,\n        });\n        await persistDb('team');\n        teamId = team.id;\n        role = 'ADMIN';\n      } else {\n        const existingTeam = db.team.findFirst({\n          where: {\n            id: {\n              equals: userObject.teamId,\n            },\n          },\n        });\n\n        if (!existingTeam) {\n          return HttpResponse.json(\n            {\n              message: 'The team you are trying to join does not exist!',\n            },\n            { status: 400 },\n          );\n        }\n        teamId = userObject.teamId;\n        role = 'USER';\n      }\n\n      db.user.create({\n        ...userObject,\n        role,\n        password: hash(userObject.password),\n        teamId,\n      });\n\n      await persistDb('user');\n\n      const result = authenticate({\n        email: userObject.email,\n        password: userObject.password,\n      });\n\n      // todo: remove once tests in Github Actions are fixed\n      Cookies.set(AUTH_COOKIE, result.jwt, { path: '/' });\n\n      return HttpResponse.json(result, {\n        headers: {\n          // with a real API servier, the token cookie should also be Secure and HttpOnly\n          'Set-Cookie': `${AUTH_COOKIE}=${result.jwt}; Path=/;`,\n        },\n      });\n    } catch (error: any) {\n      return HttpResponse.json(\n        { message: error?.message || 'Server Error' },\n        { status: 500 },\n      );\n    }\n  }),\n\n  http.post(`${env.API_URL}/auth/login`, async ({ request }) => {\n    await networkDelay();\n\n    try {\n      const credentials = (await request.json()) as LoginBody;\n      const result = authenticate(credentials);\n\n      // todo: remove once tests in Github Actions are fixed\n      Cookies.set(AUTH_COOKIE, result.jwt, { path: '/' });\n\n      return HttpResponse.json(result, {\n        headers: {\n          // with a real API servier, the token cookie should also be Secure and HttpOnly\n          'Set-Cookie': `${AUTH_COOKIE}=${result.jwt}; Path=/;`,\n        },\n      });\n    } catch (error: any) {\n      return HttpResponse.json(\n        { message: error?.message || 'Server Error' },\n        { status: 500 },\n      );\n    }\n  }),\n\n  http.post(`${env.API_URL}/auth/logout`, async () => {\n    await networkDelay();\n\n    // todo: remove once tests in Github Actions are fixed\n    Cookies.remove(AUTH_COOKIE);\n\n    return HttpResponse.json(\n      { message: 'Logged out' },\n      {\n        headers: {\n          'Set-Cookie': `${AUTH_COOKIE}=; Path=/;`,\n        },\n      },\n    );\n  }),\n\n  http.get(`${env.API_URL}/auth/me`, async ({ cookies }) => {\n    await networkDelay();\n\n    try {\n      const { user } = requireAuth(cookies);\n      return HttpResponse.json({ data: user });\n    } catch (error: any) {\n      return HttpResponse.json(\n        { message: error?.message || 'Server Error' },\n        { status: 500 },\n      );\n    }\n  }),\n];\n"
  },
  {
    "path": "apps/nextjs-pages/src/testing/mocks/handlers/comments.ts",
    "content": "import { HttpResponse, http } from 'msw';\n\nimport { env } from '@/config/env';\n\nimport { db, persistDb } from '../db';\nimport { networkDelay, requireAuth, sanitizeUser } from '../utils';\n\ntype CreateCommentBody = {\n  body: string;\n  discussionId: string;\n};\n\nexport const commentsHandlers = [\n  http.get(`${env.API_URL}/comments`, async ({ request, cookies }) => {\n    await networkDelay();\n\n    try {\n      const url = new URL(request.url);\n      const discussionId = url.searchParams.get('discussionId') || '';\n      const page = Number(url.searchParams.get('page') || 1);\n\n      const discussion = db.discussion.findFirst({\n        where: {\n          id: {\n            equals: discussionId,\n          },\n        },\n      });\n\n      if (!discussion?.public) {\n        const { error } = requireAuth(cookies);\n        if (error) {\n          return HttpResponse.json({ message: error }, { status: 401 });\n        }\n      }\n\n      const total = db.comment.count({\n        where: {\n          discussionId: {\n            equals: discussionId,\n          },\n        },\n      });\n\n      const totalPages = Math.ceil(total / 10);\n\n      const comments = db.comment\n        .findMany({\n          where: {\n            discussionId: {\n              equals: discussionId,\n            },\n          },\n          take: 10,\n          skip: 10 * (page - 1),\n        })\n        .map(({ authorId, ...comment }) => {\n          const author = db.user.findFirst({\n            where: {\n              id: {\n                equals: authorId,\n              },\n            },\n          });\n          return {\n            ...comment,\n            author: author ? sanitizeUser(author) : {},\n          };\n        });\n      return HttpResponse.json({\n        data: comments,\n        meta: {\n          page,\n          total,\n          totalPages,\n        },\n      });\n    } catch (error: any) {\n      return HttpResponse.json(\n        { message: error?.message || 'Server Error' },\n        { status: 500 },\n      );\n    }\n  }),\n\n  http.post(`${env.API_URL}/comments`, async ({ request, cookies }) => {\n    await networkDelay();\n\n    try {\n      const { user, error } = requireAuth(cookies);\n      if (error) {\n        return HttpResponse.json({ message: error }, { status: 401 });\n      }\n      const data = (await request.json()) as CreateCommentBody;\n      const result = db.comment.create({\n        authorId: user?.id,\n        ...data,\n      });\n      await persistDb('comment');\n      return HttpResponse.json(result);\n    } catch (error: any) {\n      return HttpResponse.json(\n        { message: error?.message || 'Server Error' },\n        { status: 500 },\n      );\n    }\n  }),\n\n  http.delete(\n    `${env.API_URL}/comments/:commentId`,\n    async ({ params, cookies }) => {\n      await networkDelay();\n\n      try {\n        const { user, error } = requireAuth(cookies);\n        if (error) {\n          return HttpResponse.json({ message: error }, { status: 401 });\n        }\n        const commentId = params.commentId as string;\n        const result = db.comment.delete({\n          where: {\n            id: {\n              equals: commentId,\n            },\n            ...(user?.role === 'USER' && {\n              authorId: {\n                equals: user.id,\n              },\n            }),\n          },\n        });\n        await persistDb('comment');\n        return HttpResponse.json(result);\n      } catch (error: any) {\n        return HttpResponse.json(\n          { message: error?.message || 'Server Error' },\n          { status: 500 },\n        );\n      }\n    },\n  ),\n];\n"
  },
  {
    "path": "apps/nextjs-pages/src/testing/mocks/handlers/discussions.ts",
    "content": "import { HttpResponse, http } from 'msw';\n\nimport { env } from '@/config/env';\n\nimport { db, persistDb } from '../db';\nimport {\n  requireAuth,\n  requireAdmin,\n  sanitizeUser,\n  networkDelay,\n} from '../utils';\n\ntype DiscussionBody = {\n  title: string;\n  body: string;\n  public: boolean;\n};\n\nexport const discussionsHandlers = [\n  http.get(`${env.API_URL}/discussions`, async ({ cookies, request }) => {\n    await networkDelay();\n\n    try {\n      const { user, error } = requireAuth(cookies);\n      if (error) {\n        return HttpResponse.json({ message: error }, { status: 401 });\n      }\n\n      const url = new URL(request.url);\n\n      const page = Number(url.searchParams.get('page') || 1);\n\n      const total = db.discussion.count({\n        where: {\n          teamId: {\n            equals: user?.teamId,\n          },\n        },\n      });\n\n      const totalPages = Math.ceil(total / 10);\n\n      const result = db.discussion\n        .findMany({\n          where: {\n            teamId: {\n              equals: user?.teamId,\n            },\n          },\n          take: 10,\n          skip: 10 * (page - 1),\n        })\n        .map(({ authorId, ...discussion }) => {\n          const author = db.user.findFirst({\n            where: {\n              id: {\n                equals: authorId,\n              },\n            },\n          });\n          return {\n            ...discussion,\n            author: author ? sanitizeUser(author) : {},\n          };\n        });\n      return HttpResponse.json({\n        data: result,\n        meta: {\n          page,\n          total,\n          totalPages,\n        },\n      });\n    } catch (error: any) {\n      return HttpResponse.json(\n        { message: error?.message || 'Server Error' },\n        { status: 500 },\n      );\n    }\n  }),\n\n  http.get(\n    `${env.API_URL}/discussions/:discussionId`,\n    async ({ params, cookies }) => {\n      await networkDelay();\n\n      const discussionId = params.discussionId as string;\n\n      const discussion = db.discussion.findFirst({\n        where: {\n          id: {\n            equals: discussionId,\n          },\n        },\n      });\n\n      if (discussion?.public) {\n        const author = db.user.findFirst({\n          where: {\n            id: {\n              equals: discussion.authorId,\n            },\n          },\n        });\n\n        const result = {\n          ...discussion,\n          author: author ? sanitizeUser(author) : {},\n        };\n\n        return HttpResponse.json({ data: result });\n      }\n\n      try {\n        const { user, error } = requireAuth(cookies);\n        if (error) {\n          return HttpResponse.json({ message: error }, { status: 401 });\n        }\n        const discussion = db.discussion.findFirst({\n          where: {\n            id: {\n              equals: discussionId,\n            },\n            teamId: {\n              equals: user?.teamId,\n            },\n          },\n        });\n\n        if (!discussion) {\n          return HttpResponse.json(\n            { message: 'Discussion not found' },\n            { status: 404 },\n          );\n        }\n\n        const author = db.user.findFirst({\n          where: {\n            id: {\n              equals: discussion.authorId,\n            },\n          },\n        });\n\n        const result = {\n          ...discussion,\n          author: author ? sanitizeUser(author) : {},\n        };\n\n        return HttpResponse.json({ data: result });\n      } catch (error: any) {\n        return HttpResponse.json(\n          { message: error?.message || 'Server Error' },\n          { status: 500 },\n        );\n      }\n    },\n  ),\n\n  http.post(`${env.API_URL}/discussions`, async ({ request, cookies }) => {\n    await networkDelay();\n\n    try {\n      const { user, error } = requireAuth(cookies);\n      if (error) {\n        return HttpResponse.json({ message: error }, { status: 401 });\n      }\n      const data = (await request.json()) as DiscussionBody;\n      requireAdmin(user);\n      const result = db.discussion.create({\n        teamId: user?.teamId,\n        authorId: user?.id,\n        ...data,\n      });\n      await persistDb('discussion');\n      return HttpResponse.json(result);\n    } catch (error: any) {\n      return HttpResponse.json(\n        { message: error?.message || 'Server Error' },\n        { status: 500 },\n      );\n    }\n  }),\n\n  http.patch(\n    `${env.API_URL}/discussions/:discussionId`,\n    async ({ request, params, cookies }) => {\n      await networkDelay();\n\n      try {\n        const { user, error } = requireAuth(cookies);\n        if (error) {\n          return HttpResponse.json({ message: error }, { status: 401 });\n        }\n        const data = (await request.json()) as DiscussionBody;\n        const discussionId = params.discussionId as string;\n        requireAdmin(user);\n        const result = db.discussion.update({\n          where: {\n            teamId: {\n              equals: user?.teamId,\n            },\n            id: {\n              equals: discussionId,\n            },\n          },\n          data,\n        });\n        await persistDb('discussion');\n        return HttpResponse.json(result);\n      } catch (error: any) {\n        return HttpResponse.json(\n          { message: error?.message || 'Server Error' },\n          { status: 500 },\n        );\n      }\n    },\n  ),\n\n  http.delete(\n    `${env.API_URL}/discussions/:discussionId`,\n    async ({ cookies, params }) => {\n      await networkDelay();\n\n      try {\n        const { user, error } = requireAuth(cookies);\n        if (error) {\n          return HttpResponse.json({ message: error }, { status: 401 });\n        }\n        const discussionId = params.discussionId as string;\n        requireAdmin(user);\n        const result = db.discussion.delete({\n          where: {\n            id: {\n              equals: discussionId,\n            },\n          },\n        });\n        await persistDb('discussion');\n        return HttpResponse.json(result);\n      } catch (error: any) {\n        return HttpResponse.json(\n          { message: error?.message || 'Server Error' },\n          { status: 500 },\n        );\n      }\n    },\n  ),\n];\n"
  },
  {
    "path": "apps/nextjs-pages/src/testing/mocks/handlers/index.ts",
    "content": "import { HttpResponse, http } from 'msw';\n\nimport { env } from '@/config/env';\n\nimport { networkDelay } from '../utils';\n\nimport { authHandlers } from './auth';\nimport { commentsHandlers } from './comments';\nimport { discussionsHandlers } from './discussions';\nimport { teamsHandlers } from './teams';\nimport { usersHandlers } from './users';\n\nexport const handlers = [\n  ...authHandlers,\n  ...commentsHandlers,\n  ...discussionsHandlers,\n  ...teamsHandlers,\n  ...usersHandlers,\n  http.get(`${env.API_URL}/healthcheck`, async () => {\n    await networkDelay();\n    return HttpResponse.json({ ok: true });\n  }),\n];\n"
  },
  {
    "path": "apps/nextjs-pages/src/testing/mocks/handlers/teams.ts",
    "content": "import { HttpResponse, http } from 'msw';\n\nimport { env } from '@/config/env';\n\nimport { db } from '../db';\nimport { networkDelay } from '../utils';\n\nexport const teamsHandlers = [\n  http.get(`${env.API_URL}/teams`, async () => {\n    await networkDelay();\n\n    try {\n      const result = db.team.getAll();\n      return HttpResponse.json({ data: result });\n    } catch (error: any) {\n      return HttpResponse.json(\n        { message: error?.message || 'Server Error' },\n        { status: 500 },\n      );\n    }\n  }),\n];\n"
  },
  {
    "path": "apps/nextjs-pages/src/testing/mocks/handlers/users.ts",
    "content": "import { HttpResponse, http } from 'msw';\n\nimport { env } from '@/config/env';\n\nimport { db, persistDb } from '../db';\nimport {\n  requireAuth,\n  requireAdmin,\n  sanitizeUser,\n  networkDelay,\n} from '../utils';\n\ntype ProfileBody = {\n  email: string;\n  firstName: string;\n  lastName: string;\n  bio: string;\n};\n\nexport const usersHandlers = [\n  http.get(`${env.API_URL}/users`, async ({ cookies }) => {\n    await networkDelay();\n\n    try {\n      const { user, error } = requireAuth(cookies);\n      if (error) {\n        return HttpResponse.json({ message: error }, { status: 401 });\n      }\n      const result = db.user\n        .findMany({\n          where: {\n            teamId: {\n              equals: user?.teamId,\n            },\n          },\n        })\n        .map(sanitizeUser);\n\n      return HttpResponse.json({ data: result });\n    } catch (error: any) {\n      return HttpResponse.json(\n        { message: error?.message || 'Server Error' },\n        { status: 500 },\n      );\n    }\n  }),\n\n  http.patch(`${env.API_URL}/users/profile`, async ({ request, cookies }) => {\n    await networkDelay();\n\n    try {\n      const { user, error } = requireAuth(cookies);\n      if (error) {\n        return HttpResponse.json({ message: error }, { status: 401 });\n      }\n      const data = (await request.json()) as ProfileBody;\n      const result = db.user.update({\n        where: {\n          id: {\n            equals: user?.id,\n          },\n        },\n        data,\n      });\n      await persistDb('user');\n      return HttpResponse.json(result);\n    } catch (error: any) {\n      return HttpResponse.json(\n        { message: error?.message || 'Server Error' },\n        { status: 500 },\n      );\n    }\n  }),\n\n  http.delete(`${env.API_URL}/users/:userId`, async ({ cookies, params }) => {\n    await networkDelay();\n\n    try {\n      const { user, error } = requireAuth(cookies);\n      if (error) {\n        return HttpResponse.json({ message: error }, { status: 401 });\n      }\n      const userId = params.userId as string;\n      requireAdmin(user);\n      const result = db.user.delete({\n        where: {\n          id: {\n            equals: userId,\n          },\n          teamId: {\n            equals: user?.teamId,\n          },\n        },\n      });\n      await persistDb('user');\n      return HttpResponse.json(result);\n    } catch (error: any) {\n      return HttpResponse.json(\n        { message: error?.message || 'Server Error' },\n        { status: 500 },\n      );\n    }\n  }),\n];\n"
  },
  {
    "path": "apps/nextjs-pages/src/testing/mocks/index.ts",
    "content": "import { env } from '@/config/env';\n\nexport const enableMocking = async () => {\n  if (env.ENABLE_API_MOCKING) {\n    const { worker } = await import('./browser');\n    const { initializeDb } = await import('./db');\n    await initializeDb();\n    return worker.start();\n  }\n};\n"
  },
  {
    "path": "apps/nextjs-pages/src/testing/mocks/server.ts",
    "content": "import { setupServer } from 'msw/node';\n\nimport { handlers } from './handlers';\n\nexport const server = setupServer(...handlers);\n"
  },
  {
    "path": "apps/nextjs-pages/src/testing/mocks/utils.ts",
    "content": "import Cookies from 'js-cookie';\nimport { delay } from 'msw';\n\nimport { db } from './db';\n\nexport const encode = (obj: any) => {\n  const btoa =\n    typeof window === 'undefined'\n      ? (str: string) => Buffer.from(str, 'binary').toString('base64')\n      : window.btoa;\n  return btoa(JSON.stringify(obj));\n};\n\nexport const decode = (str: string) => {\n  const atob =\n    typeof window === 'undefined'\n      ? (str: string) => Buffer.from(str, 'base64').toString('binary')\n      : window.atob;\n  return JSON.parse(atob(str));\n};\n\nexport const hash = (str: string) => {\n  let hash = 5381,\n    i = str.length;\n\n  while (i) {\n    hash = (hash * 33) ^ str.charCodeAt(--i);\n  }\n  return String(hash >>> 0);\n};\n\nexport const networkDelay = () => {\n  const delayTime = process.env.TEST\n    ? 200\n    : Math.floor(Math.random() * 700) + 300;\n  return delay(delayTime);\n};\n\nconst omit = <T extends object>(obj: T, keys: string[]): T => {\n  const result = {} as T;\n  for (const key in obj) {\n    if (!keys.includes(key)) {\n      result[key] = obj[key];\n    }\n  }\n\n  return result;\n};\n\nexport const sanitizeUser = <O extends object>(user: O) =>\n  omit<O>(user, ['password', 'iat']);\n\nexport function authenticate({\n  email,\n  password,\n}: {\n  email: string;\n  password: string;\n}) {\n  const user = db.user.findFirst({\n    where: {\n      email: {\n        equals: email,\n      },\n    },\n  });\n\n  if (user?.password === hash(password)) {\n    const sanitizedUser = sanitizeUser(user);\n    const encodedToken = encode(sanitizedUser);\n    return { user: sanitizedUser, jwt: encodedToken };\n  }\n\n  const error = new Error('Invalid username or password');\n  throw error;\n}\n\nexport const AUTH_COOKIE = `bulletproof_react_app_token`;\n\nexport function requireAuth(cookies: Record<string, string>) {\n  try {\n    const encodedToken = cookies[AUTH_COOKIE] || Cookies.get(AUTH_COOKIE);\n    if (!encodedToken) {\n      return { error: 'Unauthorized', user: null };\n    }\n    const decodedToken = decode(encodedToken) as { id: string };\n\n    const user = db.user.findFirst({\n      where: {\n        id: {\n          equals: decodedToken.id,\n        },\n      },\n    });\n\n    if (!user) {\n      return { error: 'Unauthorized', user: null };\n    }\n\n    return { user: sanitizeUser(user) };\n  } catch (err: any) {\n    return { error: 'Unauthorized', user: null };\n  }\n}\n\nexport function requireAdmin(user: any) {\n  if (user.role !== 'ADMIN') {\n    throw Error('Unauthorized');\n  }\n}\n"
  },
  {
    "path": "apps/nextjs-pages/src/testing/setup-tests.ts",
    "content": "import '@testing-library/jest-dom/vitest';\n\nimport { initializeDb, resetDb } from '@/testing/mocks/db';\nimport { server } from '@/testing/mocks/server';\n\nvi.mock('zustand');\n\nbeforeAll(() => {\n  server.listen({ onUnhandledRequest: 'error' });\n  vi.mock('next/router', () => require('next-router-mock'));\n});\nafterAll(() => server.close());\nbeforeEach(() => {\n  const ResizeObserverMock = vi.fn(() => ({\n    observe: vi.fn(),\n    unobserve: vi.fn(),\n    disconnect: vi.fn(),\n  }));\n\n  vi.stubGlobal('ResizeObserver', ResizeObserverMock);\n\n  window.btoa = (str: string) => Buffer.from(str, 'binary').toString('base64');\n  window.atob = (str: string) => Buffer.from(str, 'base64').toString('binary');\n\n  initializeDb();\n});\nafterEach(() => {\n  server.resetHandlers();\n  resetDb();\n});\n"
  },
  {
    "path": "apps/nextjs-pages/src/testing/test-utils.tsx",
    "content": "import {\n  render as rtlRender,\n  waitForElementToBeRemoved,\n  screen,\n} from '@testing-library/react';\nimport userEvent from '@testing-library/user-event';\nimport Cookies from 'js-cookie';\n\nimport { AppProvider } from '@/app/provider';\n\nimport {\n  createDiscussion as generateDiscussion,\n  createUser as generateUser,\n} from './data-generators';\nimport { db } from './mocks/db';\nimport { AUTH_COOKIE, authenticate, hash } from './mocks/utils';\n\nexport const waitForLoadingToFinish = () =>\n  waitForElementToBeRemoved(\n    () => [\n      ...screen.queryAllByTestId(/loading/i),\n      ...screen.queryAllByText(/loading/i),\n    ],\n    { timeout: 4000 },\n  );\n\nexport const createUser = async (userProperties?: any) => {\n  const user = generateUser(userProperties) as any;\n  await db.user.create({ ...user, password: hash(user.password) });\n  return user;\n};\n\nexport const createDiscussion = async (discussionProperties?: any) => {\n  const discussion = generateDiscussion(discussionProperties);\n  const res = await db.discussion.create(discussion);\n  return res;\n};\n\nexport const loginAsUser = async (user: any) => {\n  const authUser = await authenticate(user);\n  Cookies.set(AUTH_COOKIE, authUser.jwt);\n  return authUser;\n};\n\nconst initializeUser = async (user: any) => {\n  if (typeof user === 'undefined') {\n    const newUser = await createUser();\n    return loginAsUser(newUser);\n  } else if (user) {\n    return loginAsUser(user);\n  } else {\n    return null;\n  }\n};\n\nexport const renderApp = async (\n  ui: any,\n  { user, ...renderOptions }: Record<string, any> = {},\n) => {\n  // if you want to render the app unauthenticated then pass \"null\" as the user\n  const initializedUser = await initializeUser(user);\n\n  const returnValue = {\n    ...rtlRender(ui, {\n      wrapper: AppProvider,\n      ...renderOptions,\n    }),\n    user: initializedUser,\n  };\n\n  return returnValue;\n};\n\nexport * from '@testing-library/react';\nexport { userEvent, rtlRender };\n"
  },
  {
    "path": "apps/nextjs-pages/src/types/api.ts",
    "content": "// let's imagine this file is autogenerated from the backend\n// ideally, we want to keep these api related types in sync\n// with the backend instead of manually writing them out\n\nexport type BaseEntity = {\n  id: string;\n  createdAt: number;\n};\n\nexport type Entity<T> = {\n  [K in keyof T]: T[K];\n} & BaseEntity;\n\nexport type Meta = {\n  page: number;\n  total: number;\n  totalPages: number;\n};\n\nexport type User = Entity<{\n  firstName: string;\n  lastName: string;\n  email: string;\n  role: 'ADMIN' | 'USER';\n  teamId: string;\n  bio: string;\n}>;\n\nexport type AuthResponse = {\n  jwt: string;\n  user: User;\n};\n\nexport type Team = Entity<{\n  name: string;\n  description: string;\n}>;\n\nexport type Discussion = Entity<{\n  title: string;\n  body: string;\n  teamId: string;\n  author: User;\n  public: boolean;\n}>;\n\nexport type Comment = Entity<{\n  body: string;\n  discussionId: string;\n  author: User;\n}>;\n"
  },
  {
    "path": "apps/nextjs-pages/src/utils/cn.ts",
    "content": "import { type ClassValue, clsx } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"
  },
  {
    "path": "apps/nextjs-pages/src/utils/format.ts",
    "content": "import { default as dayjs } from 'dayjs';\n\nexport const formatDate = (date: number) =>\n  dayjs(date).format('MMMM D, YYYY h:mm A');\n"
  },
  {
    "path": "apps/nextjs-pages/tailwind.config.cjs",
    "content": "/** @type {import('tailwindcss').Config} */\n\nconst defaultTheme = require('tailwindcss/defaultTheme');\n\nmodule.exports = {\n  content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],\n  theme: {\n    container: {\n      center: true,\n      padding: '2rem',\n      screens: {\n        '2xl': '1400px',\n      },\n    },\n    extend: {\n      fontFamily: {\n        sans: ['Inter var', ...defaultTheme.fontFamily.sans],\n      },\n      colors: {\n        border: 'hsl(var(--border))',\n        input: 'hsl(var(--input))',\n        ring: 'hsl(var(--ring))',\n        background: 'hsl(var(--background))',\n        foreground: 'hsl(var(--foreground))',\n        primary: {\n          DEFAULT: 'hsl(var(--primary))',\n          foreground: 'hsl(var(--primary-foreground))',\n        },\n        secondary: {\n          DEFAULT: 'hsl(var(--secondary))',\n          foreground: 'hsl(var(--secondary-foreground))',\n        },\n        destructive: {\n          DEFAULT: 'hsl(var(--destructive))',\n          foreground: 'hsl(var(--destructive-foreground))',\n        },\n        muted: {\n          DEFAULT: 'hsl(var(--muted))',\n          foreground: 'hsl(var(--muted-foreground))',\n        },\n        accent: {\n          DEFAULT: 'hsl(var(--accent))',\n          foreground: 'hsl(var(--accent-foreground))',\n        },\n        popover: {\n          DEFAULT: 'hsl(var(--popover))',\n          foreground: 'hsl(var(--popover-foreground))',\n        },\n        card: {\n          DEFAULT: 'hsl(var(--card))',\n          foreground: 'hsl(var(--card-foreground))',\n        },\n      },\n      borderRadius: {\n        lg: 'var(--radius)',\n        md: 'calc(var(--radius) - 2px)',\n        sm: 'calc(var(--radius) - 4px)',\n      },\n      keyframes: {\n        'accordion-down': {\n          from: { height: '0' },\n          to: { height: 'var(--radix-accordion-content-height)' },\n        },\n        'accordion-up': {\n          from: { height: 'var(--radix-accordion-content-height)' },\n          to: { height: '0' },\n        },\n      },\n      animation: {\n        'accordion-down': 'accordion-down 0.2s ease-out',\n        'accordion-up': 'accordion-up 0.2s ease-out',\n      },\n    },\n  },\n  plugins: [require('tailwindcss-animate'), require('@tailwindcss/typography')],\n};\n"
  },
  {
    "path": "apps/nextjs-pages/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"target\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"incremental\": true,\n    \"paths\": {\n      \"@/*\": [\"./src/*\"]\n    },\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ]\n  },\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".next/types/**/*.ts\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "apps/nextjs-pages/vitest.config.ts",
    "content": "/// <reference types=\"vitest\" />\n\nimport react from '@vitejs/plugin-react';\nimport viteTsconfigPaths from 'vite-tsconfig-paths';\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  base: './',\n  plugins: [react(), viteTsconfigPaths()],\n  test: {\n    globals: true,\n    environment: 'jsdom',\n    setupFiles: './src/testing/setup-tests.ts',\n    exclude: ['**/node_modules/**', '**/e2e/**'],\n    coverage: {\n      include: ['src/**'],\n    },\n  },\n});\n"
  },
  {
    "path": "apps/react-vite/.eslintrc.cjs",
    "content": "module.exports = {\n  root: true,\n  env: {\n    node: true,\n    es6: true,\n  },\n  parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },\n  ignorePatterns: [\n    'node_modules/*',\n    'public/mockServiceWorker.js',\n    'generators/*',\n  ],\n  extends: ['eslint:recommended'],\n  plugins: ['check-file'],\n  overrides: [\n    {\n      files: ['**/*.ts', '**/*.tsx'],\n      parser: '@typescript-eslint/parser',\n      settings: {\n        react: { version: 'detect' },\n        'import/resolver': {\n          typescript: {},\n        },\n      },\n      env: {\n        browser: true,\n        node: true,\n        es6: true,\n      },\n      extends: [\n        'eslint:recommended',\n        'plugin:import/errors',\n        'plugin:import/warnings',\n        'plugin:import/typescript',\n        'plugin:@typescript-eslint/recommended',\n        'plugin:react/recommended',\n        'plugin:react-hooks/recommended',\n        'plugin:jsx-a11y/recommended',\n        'plugin:prettier/recommended',\n        'plugin:testing-library/react',\n        'plugin:jest-dom/recommended',\n        'plugin:tailwindcss/recommended',\n        'plugin:vitest/legacy-recommended',\n      ],\n      rules: {\n        'import/no-restricted-paths': [\n          'error',\n          {\n            zones: [\n              // disables cross-feature imports:\n              // eg. src/features/discussions should not import from src/features/comments, etc.\n              {\n                target: './src/features/auth',\n                from: './src/features',\n                except: ['./auth'],\n              },\n              {\n                target: './src/features/comments',\n                from: './src/features',\n                except: ['./comments'],\n              },\n              {\n                target: './src/features/discussions',\n                from: './src/features',\n                except: ['./discussions'],\n              },\n              {\n                target: './src/features/teams',\n                from: './src/features',\n                except: ['./teams'],\n              },\n              {\n                target: './src/features/users',\n                from: './src/features',\n                except: ['./users'],\n              },\n              // enforce unidirectional codebase:\n\n              // e.g. src/app can import from src/features but not the other way around\n              {\n                target: './src/features',\n                from: './src/app',\n              },\n\n              // e.g src/features and src/app can import from these shared modules but not the other way around\n              {\n                target: [\n                  './src/components',\n                  './src/hooks',\n                  './src/lib',\n                  './src/types',\n                  './src/utils',\n                ],\n                from: ['./src/features', './src/app'],\n              },\n            ],\n          },\n        ],\n        'import/no-cycle': 'error',\n        'linebreak-style': ['error', 'unix'],\n        'react/prop-types': 'off',\n        'import/order': [\n          'error',\n          {\n            groups: [\n              'builtin',\n              'external',\n              'internal',\n              'parent',\n              'sibling',\n              'index',\n              'object',\n            ],\n            'newlines-between': 'always',\n            alphabetize: { order: 'asc', caseInsensitive: true },\n          },\n        ],\n        'import/default': 'off',\n        'import/no-named-as-default-member': 'off',\n        'import/no-named-as-default': 'off',\n        'react/react-in-jsx-scope': 'off',\n        'jsx-a11y/anchor-is-valid': 'off',\n        '@typescript-eslint/no-unused-vars': ['error'],\n        '@typescript-eslint/explicit-function-return-type': ['off'],\n        '@typescript-eslint/explicit-module-boundary-types': ['off'],\n        '@typescript-eslint/no-empty-function': ['off'],\n        '@typescript-eslint/no-explicit-any': ['off'],\n        'prettier/prettier': ['error', {}, { usePrettierrc: true }],\n        'check-file/filename-naming-convention': [\n          'error',\n          {\n            '**/*.{ts,tsx}': 'KEBAB_CASE',\n          },\n          {\n            ignoreMiddleExtensions: true,\n          },\n        ],\n      },\n    },\n    {\n      plugins: ['check-file'],\n      files: ['src/**/!(__tests__)/*'],\n      rules: {\n        'check-file/folder-naming-convention': [\n          'error',\n          {\n            '**/*': 'KEBAB_CASE',\n          },\n        ],\n      },\n    },\n  ],\n};\n"
  },
  {
    "path": "apps/react-vite/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n/test-results/\n/playwright-report/\n/blob-report/\n/playwright/.cache/\n/e2e/.auth/\n\n# storybook\nmigration-storybook.log\nstorybook.log\nstorybook-static\n\n\n# production\n/dist\n\n# misc\n.DS_Store\n.env\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n\n# local\nmocked-db.json\n\n"
  },
  {
    "path": "apps/react-vite/.prettierignore",
    "content": "*.hbs"
  },
  {
    "path": "apps/react-vite/.prettierrc",
    "content": "{\n  \"singleQuote\": true,\n  \"trailingComma\": \"all\",\n  \"printWidth\": 80,\n  \"tabWidth\": 2,\n  \"useTabs\": false\n}\n"
  },
  {
    "path": "apps/react-vite/.storybook/main.ts",
    "content": "module.exports = {\n  stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],\n\n  addons: [\n    '@storybook/addon-actions',\n    '@storybook/addon-links',\n    '@storybook/node-logger',\n    '@storybook/addon-essentials',\n    '@storybook/addon-interactions',\n    '@storybook/addon-docs',\n    '@storybook/addon-a11y',\n  ],\n  framework: {\n    name: '@storybook/react-vite',\n    options: {},\n  },\n  docs: {\n    autodocs: 'tag',\n  },\n  typescript: {\n    reactDocgen: 'react-docgen-typescript',\n  },\n};\n"
  },
  {
    "path": "apps/react-vite/.storybook/preview.tsx",
    "content": "import React from 'react';\nimport { BrowserRouter as Router } from 'react-router';\nimport '../src/index.css';\n\nexport const parameters = {\n  actions: { argTypesRegex: '^on[A-Z].*' },\n};\n\nexport const decorators = [\n  (Story) => (\n    <Router>\n      <Story />\n    </Router>\n  ),\n];\n"
  },
  {
    "path": "apps/react-vite/.vscode/extensions.json",
    "content": "{\n  \"recommendations\": [\n    \"dbaeumer.vscode-eslint\",\n    \"esbenp.prettier-vscode\",\n    \"dsznajder.es7-react-js-snippets\",\n    \"mariusalchimavicius.json-to-ts\",\n    \"bradlc.vscode-tailwindcss\"\n  ]\n}\n"
  },
  {
    "path": "apps/react-vite/.vscode/settings.json",
    "content": "{\n  \"editor.formatOnSave\": true,\n  \"editor.codeActionsOnSave\": {\n    \"source.fixAll.eslint\": \"explicit\"\n  }\n}\n"
  },
  {
    "path": "apps/react-vite/README.md",
    "content": "# React Vite Application\n\n## Get Started\n\nPrerequisites:\n\n- Node 20+\n- Yarn 1.22+\n\nTo set up the app execute the following commands.\n\n```bash\ngit clone https://github.com/alan2207/bulletproof-react.git\ncd bulletproof-react\ncd apps/react-vite\ncp .env.example .env\nyarn install\n```\n\n##### `yarn dev`\n\nRuns the app in the development mode.\\\nOpen [http://localhost:3000](http://localhost:3000) to view it in the browser.\n\n##### `yarn build`\n\nBuilds the app for production to the `dist` folder.\\\nIt correctly bundles React in production mode and optimizes the build for the best performance.\n\nSee the section about [deployment](https://vitejs.dev/guide/static-deploy) for more information.\n"
  },
  {
    "path": "apps/react-vite/__mocks__/vitest-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n/// <reference types=\"vitest/globals\" />\n"
  },
  {
    "path": "apps/react-vite/__mocks__/zustand.ts",
    "content": "import { act } from '@testing-library/react';\nimport { afterEach, vi } from 'vitest';\nimport * as zustand from 'zustand';\n\nconst { create: actualCreate, createStore: actualCreateStore } =\n  await vi.importActual<typeof zustand>('zustand');\n\n// a variable to hold reset functions for all stores declared in the app\nexport const storeResetFns = new Set<() => void>();\n\nconst createUncurried = <T>(stateCreator: zustand.StateCreator<T>) => {\n  const store = actualCreate(stateCreator);\n  const initialState = store.getInitialState();\n  storeResetFns.add(() => {\n    store.setState(initialState, true);\n  });\n  return store;\n};\n\n// when creating a store, we get its initial state, create a reset function and add it in the set\nexport const create = (<T>(stateCreator: zustand.StateCreator<T>) => {\n  // to support curried version of create\n  return typeof stateCreator === 'function'\n    ? createUncurried(stateCreator)\n    : createUncurried;\n}) as typeof zustand.create;\n\nconst createStoreUncurried = <T>(stateCreator: zustand.StateCreator<T>) => {\n  const store = actualCreateStore(stateCreator);\n  const initialState = store.getInitialState();\n  storeResetFns.add(() => {\n    store.setState(initialState, true);\n  });\n  return store;\n};\n\n// when creating a store, we get its initial state, create a reset function and add it in the set\nexport const createStore = (<T>(stateCreator: zustand.StateCreator<T>) => {\n  // to support curried version of createStore\n  return typeof stateCreator === 'function'\n    ? createStoreUncurried(stateCreator)\n    : createStoreUncurried;\n}) as typeof zustand.createStore;\n\n// reset all stores after each test run\nafterEach(() => {\n  act(() => {\n    storeResetFns.forEach((resetFn) => {\n      resetFn();\n    });\n  });\n});\n"
  },
  {
    "path": "apps/react-vite/e2e/.eslintrc.cjs",
    "content": "module.exports = {\n  root: true,\n  parser: '@typescript-eslint/parser',\n  extends: 'plugin:playwright/recommended',\n};\n"
  },
  {
    "path": "apps/react-vite/e2e/tests/auth.setup.ts",
    "content": "import { test as setup, expect } from '@playwright/test';\nimport { createUser } from '../../src/testing/data-generators';\n\nconst authFile = 'e2e/.auth/user.json';\n\nsetup('authenticate', async ({ page }) => {\n  const user = createUser();\n\n  await page.goto('/');\n  await page.getByRole('button', { name: 'Get started' }).click();\n  await page.waitForURL('/auth/login');\n  await page.getByRole('link', { name: 'Register' }).click();\n\n  // registration:\n  await page.getByLabel('First Name').click();\n  await page.getByLabel('First Name').fill(user.firstName);\n  await page.getByLabel('Last Name').click();\n  await page.getByLabel('Last Name').fill(user.lastName);\n  await page.getByLabel('Email Address').click();\n  await page.getByLabel('Email Address').fill(user.email);\n  await page.getByLabel('Password').click();\n  await page.getByLabel('Password').fill(user.password);\n  await page.getByLabel('Team Name').click();\n  await page.getByLabel('Team Name').fill(user.teamName);\n  await page.getByRole('button', { name: 'Register' }).click();\n  await page.waitForURL('/app');\n\n  // log out:\n  await page.getByRole('button', { name: 'Open user menu' }).click();\n  await page.getByRole('menuitem', { name: 'Sign Out' }).click();\n  await page.waitForURL('/auth/login?redirectTo=%2Fapp');\n\n  // log in:\n  await page.getByLabel('Email Address').click();\n  await page.getByLabel('Email Address').fill(user.email);\n  await page.getByLabel('Password').click();\n  await page.getByLabel('Password').fill(user.password);\n  await page.getByRole('button', { name: 'Log in' }).click();\n  await page.waitForURL('/app');\n\n  await page.context().storageState({ path: authFile });\n});\n"
  },
  {
    "path": "apps/react-vite/e2e/tests/profile.spec.ts",
    "content": "import { test, expect } from '@playwright/test';\n\ntest('profile', async ({ page }) => {\n  // update user:\n  await page.goto('/app');\n  await page.getByRole('button', { name: 'Open user menu' }).click();\n  await page.getByRole('menuitem', { name: 'Your Profile' }).click();\n  await page.getByRole('button', { name: 'Update Profile' }).click();\n  await page.getByLabel('Bio').click();\n  await page.getByLabel('Bio').fill('My bio');\n  await page.getByRole('button', { name: 'Submit' }).click();\n  await page\n    .getByLabel('Profile Updated')\n    .getByRole('button', { name: 'Close' })\n    .click();\n  await expect(page.getByText('My bio')).toBeVisible();\n});\n"
  },
  {
    "path": "apps/react-vite/e2e/tests/smoke.spec.ts",
    "content": "import { test, expect } from '@playwright/test';\n\nimport {\n  createDiscussion,\n  createComment,\n} from '../../src/testing/data-generators';\ntest('smoke', async ({ page }) => {\n  const discussion = createDiscussion();\n  const comment = createComment();\n\n  await page.goto('/');\n  await page.getByRole('button', { name: 'Get started' }).click();\n  await page.waitForURL('/app');\n\n  // create discussion:\n  await page.getByRole('link', { name: 'Discussions' }).click();\n  await page.waitForURL('/app/discussions');\n\n  await page.getByRole('button', { name: 'Create Discussion' }).click();\n  await page.getByLabel('Title').click();\n  await page.getByLabel('Title').fill(discussion.title);\n  await page.getByLabel('Body').click();\n  await page.getByLabel('Body').fill(discussion.body);\n  await page.getByRole('button', { name: 'Submit' }).click();\n  await page\n    .getByLabel('Discussion Created')\n    .getByRole('button', { name: 'Close' })\n    .click();\n\n  // visit discussion page:\n  await page.getByRole('link', { name: 'View' }).click();\n\n  await expect(\n    page.getByRole('heading', { name: discussion.title }),\n  ).toBeVisible();\n  await expect(page.getByText(discussion.body)).toBeVisible();\n\n  // update discussion:\n  await page.getByRole('button', { name: 'Update Discussion' }).click();\n  await page.getByLabel('Title').click();\n  await page.getByLabel('Title').fill(`${discussion.title} - updated`);\n  await page.getByLabel('Body').click();\n  await page.getByLabel('Body').fill(`${discussion.body} - updated`);\n  await page.getByRole('button', { name: 'Submit' }).click();\n  await page\n    .getByLabel('Discussion Updated')\n    .getByRole('button', { name: 'Close' })\n    .click();\n\n  await expect(\n    page.getByRole('heading', { name: `${discussion.title} - updated` }),\n  ).toBeVisible();\n  await expect(page.getByText(`${discussion.body} - updated`)).toBeVisible();\n\n  // create comment:\n  await page.getByRole('button', { name: 'Create Comment' }).click();\n  await page.getByLabel('Body').click();\n  await page.getByLabel('Body').fill(comment.body);\n  await page.getByRole('button', { name: 'Submit' }).click();\n  await expect(page.getByText(comment.body)).toBeVisible();\n  await page\n    .getByLabel('Comment Created')\n    .getByRole('button', { name: 'Close' })\n    .click();\n\n  // delete comment:\n  await page.getByRole('button', { name: 'Delete Comment' }).click();\n  await expect(\n    page.getByText('Are you sure you want to delete this comment?'),\n  ).toBeVisible();\n  await page.getByRole('button', { name: 'Delete Comment' }).click();\n  await page\n    .getByLabel('Comment Deleted')\n    .getByRole('button', { name: 'Close' })\n    .click();\n  await expect(\n    page.getByRole('heading', { name: 'No Comments Found' }),\n  ).toBeVisible();\n  await expect(page.getByText(comment.body)).toBeHidden();\n\n  // go back to discussions:\n  await page.getByRole('link', { name: 'Discussions' }).click();\n  await page.waitForURL('/app/discussions');\n\n  // delete discussion:\n  await page.getByRole('button', { name: 'Delete Discussion' }).click();\n  await page.getByRole('button', { name: 'Delete Discussion' }).click();\n  await page\n    .getByLabel('Discussion Deleted')\n    .getByRole('button', { name: 'Close' })\n    .click();\n  await expect(\n    page.getByRole('heading', { name: 'No Entries Found' }),\n  ).toBeVisible();\n});\n"
  },
  {
    "path": "apps/react-vite/generators/component/component.stories.tsx.hbs",
    "content": "import { Meta, StoryObj } from '@storybook/react';\n\nimport { {{ properCase name }} } from './{{ kebabCase name }}';\n\nconst meta: Meta<typeof {{ properCase name }}> = {\n  component: {{ properCase name }},\n};\n\nexport default meta;\n\ntype Story = StoryObj<typeof {{ properCase name }}>;\n\nexport const Default: Story = {\n  args: {}\n};\n"
  },
  {
    "path": "apps/react-vite/generators/component/component.tsx.hbs",
    "content": "import * as React from \"react\"; \n\nexport type {{properCase name}}Props = {};\n\nexport const {{properCase name}} = (props: {{properCase name}}Props) => { \n  return (\n    <div>\n      {{properCase name}}\n    </div>\n  ); \n};"
  },
  {
    "path": "apps/react-vite/generators/component/index.cjs",
    "content": "const path = require('path');\nconst fs = require('fs');\n\nconst featuresDir = path.join(process.cwd(), 'src/features');\nconst features = fs.readdirSync(featuresDir);\n\n/**\n *\n * @type {import('plop').PlopGenerator}\n */\nmodule.exports = {\n  description: 'Component Generator',\n  prompts: [\n    {\n      type: 'input',\n      name: 'name',\n      message: 'component name',\n    },\n    {\n      type: 'list',\n      name: 'feature',\n      message: 'Which feature does this component belong to?',\n      choices: ['components', ...features],\n      when: () => features.length > 0,\n    },\n    {\n      type: 'input',\n      name: 'folder',\n      message: 'folder in components',\n      when: ({ feature }) => !feature || feature === 'components',\n    },\n  ],\n  actions: (answers) => {\n    const componentGeneratePath =\n      !answers.feature || answers.feature === 'components'\n        ? 'src/components/{{folder}}'\n        : 'src/features/{{feature}}/components';\n    return [\n      {\n        type: 'add',\n        path: componentGeneratePath + '/{{kebabCase name}}/index.ts',\n        templateFile: 'generators/component/index.ts.hbs',\n      },\n      {\n        type: 'add',\n        path: componentGeneratePath + '/{{kebabCase name}}/{{kebabCase name}}.tsx',\n        templateFile: 'generators/component/component.tsx.hbs',\n      },\n      {\n        type: 'add',\n        path: componentGeneratePath + '/{{kebabCase name}}/{{kebabCase name}}.stories.tsx',\n        templateFile: 'generators/component/component.stories.tsx.hbs',\n      },\n    ];\n  },\n};\n"
  },
  {
    "path": "apps/react-vite/generators/component/index.ts.hbs",
    "content": "export * from './{{ kebabCase name }}';\n"
  },
  {
    "path": "apps/react-vite/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <link rel=\"icon\" href=\"/favicon.ico\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <meta name=\"theme-color\" content=\"#000000\" />\n    <meta name=\"description\" content=\"Bulletproof React Application\" />\n    <link rel=\"stylesheet\" href=\"https://rsms.me/inter/inter.css\" />\n\n    <title>Bulletproof React</title>\n  </head>\n  <body>\n    <noscript>You need to enable JavaScript to run this app.</noscript>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/react-vite/mock-server.ts",
    "content": "import { createMiddleware } from '@mswjs/http-middleware';\nimport cors from 'cors';\nimport express from 'express';\nimport logger from 'pino-http';\n\nimport { env } from './src/config/env';\nimport { initializeDb } from './src/testing/mocks/db';\nimport { handlers } from './src/testing/mocks/handlers';\n\nconst app = express();\n\napp.use(\n  cors({\n    origin: env.APP_URL,\n    credentials: true,\n  }),\n);\n\napp.use(express.json());\napp.use(logger());\napp.use(createMiddleware(...handlers));\n\ninitializeDb().then(() => {\n  console.log('Mock DB initialized');\n  app.listen(env.APP_MOCK_API_PORT, () => {\n    console.log(\n      `Mock API server started at http://localhost:${env.APP_MOCK_API_PORT}`,\n    );\n  });\n});\n"
  },
  {
    "path": "apps/react-vite/package.json",
    "content": "{\n  \"name\": \"bulletproof-react-vite\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc && vite build --base=/\",\n    \"preview\": \"vite preview\",\n    \"test\": \"vitest\",\n    \"test-e2e\": \"pm2 start \\\"yarn run-mock-server\\\" --name server && yarn playwright test\",\n    \"prepare\": \"husky\",\n    \"lint\": \"eslint src --ignore-path .gitignore\",\n    \"check-types\": \"tsc --project tsconfig.json --pretty --noEmit\",\n    \"generate\": \"plop\",\n    \"storybook\": \"storybook dev -p 6006\",\n    \"build-storybook\": \"storybook build\",\n    \"run-mock-server\": \"vite-node mock-server.ts | pino-pretty -c\"\n  },\n  \"dependencies\": {\n    \"@hookform/resolvers\": \"^3.3.4\",\n    \"@ngneat/falso\": \"^7.2.0\",\n    \"@radix-ui/react-dialog\": \"^1.0.5\",\n    \"@radix-ui/react-dropdown-menu\": \"^2.0.6\",\n    \"@radix-ui/react-icons\": \"^1.3.0\",\n    \"@radix-ui/react-label\": \"^2.0.2\",\n    \"@radix-ui/react-slot\": \"^1.0.2\",\n    \"@radix-ui/react-switch\": \"^1.0.3\",\n    \"@tanstack/react-query\": \"^5.32.0\",\n    \"@tanstack/react-query-devtools\": \"^5.32.0\",\n    \"axios\": \"^1.6.8\",\n    \"class-variance-authority\": \"^0.7.0\",\n    \"clsx\": \"^2.1.1\",\n    \"dayjs\": \"^1.11.11\",\n    \"dompurify\": \"^3.1.1\",\n    \"eslint-plugin-check-file\": \"^2.8.0\",\n    \"lucide-react\": \"^0.378.0\",\n    \"marked\": \"^12.0.2\",\n    \"nanoid\": \"^5.0.7\",\n    \"react\": \"^18.3.1\",\n    \"react-dom\": \"^18.3.1\",\n    \"react-error-boundary\": \"^4.0.13\",\n    \"react-helmet-async\": \"^2.0.4\",\n    \"react-hook-form\": \"^7.51.3\",\n    \"react-query-auth\": \"^2.4.3\",\n    \"react-router\": \"^7.0.2\",\n    \"tailwind-merge\": \"^2.3.0\",\n    \"tailwindcss-animate\": \"^1.0.7\",\n    \"zod\": \"^3.23.4\",\n    \"zustand\": \"^4.5.2\"\n  },\n  \"devDependencies\": {\n    \"@eslint/eslintrc\": \"^3.0.2\",\n    \"@mswjs/data\": \"^0.16.1\",\n    \"@mswjs/http-middleware\": \"^0.10.1\",\n    \"@playwright/test\": \"^1.43.1\",\n    \"@storybook/addon-a11y\": \"^8.0.10\",\n    \"@storybook/addon-actions\": \"^8.0.9\",\n    \"@storybook/addon-essentials\": \"^8.0.9\",\n    \"@storybook/addon-links\": \"^8.0.9\",\n    \"@storybook/node-logger\": \"^8.0.9\",\n    \"@storybook/react\": \"^8.0.9\",\n    \"@storybook/react-vite\": \"^8.0.9\",\n    \"@tailwindcss/typography\": \"^0.5.13\",\n    \"@testing-library/jest-dom\": \"^6.4.2\",\n    \"@testing-library/react\": \"^15.0.5\",\n    \"@testing-library/user-event\": \"^14.5.2\",\n    \"@types/cors\": \"^2.8.17\",\n    \"@types/dompurify\": \"^3.0.5\",\n    \"@types/js-cookie\": \"^3.0.6\",\n    \"@types/marked\": \"^6.0.0\",\n    \"@types/node\": \"^20.12.7\",\n    \"@types/react\": \"^18.3.1\",\n    \"@types/react-dom\": \"^18.3.0\",\n    \"@typescript-eslint/eslint-plugin\": \"^7.8.0\",\n    \"@typescript-eslint/parser\": \"^7.8.0\",\n    \"@vitejs/plugin-react\": \"^4.2.1\",\n    \"autoprefixer\": \"^10.4.19\",\n    \"cors\": \"^2.8.5\",\n    \"eslint\": \"8\",\n    \"eslint-config-prettier\": \"^9.1.0\",\n    \"eslint-import-resolver-typescript\": \"^3.6.1\",\n    \"eslint-plugin-import\": \"^2.29.1\",\n    \"eslint-plugin-jest-dom\": \"^5.4.0\",\n    \"eslint-plugin-jsx-a11y\": \"^6.8.0\",\n    \"eslint-plugin-playwright\": \"^1.6.0\",\n    \"eslint-plugin-prettier\": \"^5.1.3\",\n    \"eslint-plugin-react\": \"^7.34.1\",\n    \"eslint-plugin-react-hooks\": \"^4.6.2\",\n    \"eslint-plugin-tailwindcss\": \"^3.15.1\",\n    \"eslint-plugin-testing-library\": \"^6.2.2\",\n    \"eslint-plugin-vitest\": \"^0.5.4\",\n    \"express\": \"^4.19.2\",\n    \"husky\": \"^9.0.11\",\n    \"jest-environment-jsdom\": \"^29.7.0\",\n    \"js-cookie\": \"^3.0.5\",\n    \"jsdom\": \"^24.0.0\",\n    \"lint-staged\": \"^15.2.2\",\n    \"msw\": \"^2.2.14\",\n    \"pino-http\": \"^10.1.0\",\n    \"pino-pretty\": \"^11.1.0\",\n    \"plop\": \"^4.0.1\",\n    \"pm2\": \"^5.4.0\",\n    \"postcss\": \"^8.4.38\",\n    \"prettier\": \"^3.2.5\",\n    \"storybook\": \"^8.0.9\",\n    \"tailwindcss\": \"^3.4.3\",\n    \"typescript\": \"^5.4.5\",\n    \"vite\": \"^5.2.10\",\n    \"vite-node\": \"^1.6.0\",\n    \"vite-tsconfig-paths\": \"^4.3.2\",\n    \"vitest\": \"^2.1.4\"\n  },\n  \"msw\": {\n    \"workerDirectory\": \"public\"\n  },\n  \"lint-staged\": {\n    \"*.+(ts|tsx)\": [\n      \"yarn lint\",\n      \"bash -c 'yarn check-types'\"\n    ]\n  }\n}\n"
  },
  {
    "path": "apps/react-vite/playwright.config.ts",
    "content": "import { defineConfig, devices } from '@playwright/test';\n\nconst PORT = 3000;\n\n/**\n * Read environment variables from file.\n * https://github.com/motdotla/dotenv\n */\n// require('dotenv').config();\n\n/**\n * See https://playwright.dev/docs/test-configuration.\n */\nexport default defineConfig({\n  testDir: './e2e',\n  /* Run tests in files in parallel */\n  fullyParallel: true,\n  /* Fail the build on CI if you accidentally left test.only in the source code. */\n  forbidOnly: !!process.env.CI,\n  /* Retry on CI only */\n  retries: process.env.CI ? 2 : 0,\n  /* Opt out of parallel tests on CI. */\n  workers: process.env.CI ? 1 : undefined,\n  /* Reporter to use. See https://playwright.dev/docs/test-reporters */\n  reporter: 'html',\n  /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */\n  use: {\n    /* Base URL to use in actions like `await page.goto('/')`. */\n    // baseURL: 'http://127.0.0.1:3000',\n\n    /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */\n    trace: 'on-first-retry',\n  },\n\n  /* Configure projects for major browsers */\n  projects: [\n    { name: 'setup', testMatch: /.*\\.setup\\.ts/ },\n    {\n      name: 'chromium',\n      testMatch: /.*\\.spec\\.ts/,\n      use: {\n        ...devices['Desktop Chrome'],\n        storageState: 'e2e/.auth/user.json',\n      },\n      dependencies: ['setup'],\n    },\n  ],\n\n  /* Run your local dev server before starting the tests */\n  webServer: {\n    command: `yarn dev --port ${PORT}`,\n    timeout: 10 * 1000,\n    port: PORT,\n    reuseExistingServer: !process.env.CI,\n  },\n});\n"
  },
  {
    "path": "apps/react-vite/plopfile.cjs",
    "content": "const componentGenerator = require('./generators/component/index');\n\n/**\n *\n * @param {import('plop').NodePlopAPI} plop\n */\nmodule.exports = function (plop) {\n  plop.setGenerator('component', componentGenerator);\n};\n"
  },
  {
    "path": "apps/react-vite/postcss.config.cjs",
    "content": "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n};\n"
  },
  {
    "path": "apps/react-vite/public/_redirects",
    "content": "/* /index.html 200"
  },
  {
    "path": "apps/react-vite/public/mockServiceWorker.js",
    "content": "/* eslint-disable */\n/* tslint:disable */\n\n/**\n * Mock Service Worker.\n * @see https://github.com/mswjs/msw\n * - Please do NOT modify this file.\n * - Please do NOT serve this file on production.\n */\n\nconst PACKAGE_VERSION = '2.2.14'\nconst INTEGRITY_CHECKSUM = '26357c79639bfa20d64c0efca2a87423'\nconst IS_MOCKED_RESPONSE = Symbol('isMockedResponse')\nconst activeClientIds = new Set()\n\nself.addEventListener('install', function () {\n  self.skipWaiting()\n})\n\nself.addEventListener('activate', function (event) {\n  event.waitUntil(self.clients.claim())\n})\n\nself.addEventListener('message', async function (event) {\n  const clientId = event.source.id\n\n  if (!clientId || !self.clients) {\n    return\n  }\n\n  const client = await self.clients.get(clientId)\n\n  if (!client) {\n    return\n  }\n\n  const allClients = await self.clients.matchAll({\n    type: 'window',\n  })\n\n  switch (event.data) {\n    case 'KEEPALIVE_REQUEST': {\n      sendToClient(client, {\n        type: 'KEEPALIVE_RESPONSE',\n      })\n      break\n    }\n\n    case 'INTEGRITY_CHECK_REQUEST': {\n      sendToClient(client, {\n        type: 'INTEGRITY_CHECK_RESPONSE',\n        payload: {\n          packageVersion: PACKAGE_VERSION,\n          checksum: INTEGRITY_CHECKSUM,\n        },\n      })\n      break\n    }\n\n    case 'MOCK_ACTIVATE': {\n      activeClientIds.add(clientId)\n\n      sendToClient(client, {\n        type: 'MOCKING_ENABLED',\n        payload: true,\n      })\n      break\n    }\n\n    case 'MOCK_DEACTIVATE': {\n      activeClientIds.delete(clientId)\n      break\n    }\n\n    case 'CLIENT_CLOSED': {\n      activeClientIds.delete(clientId)\n\n      const remainingClients = allClients.filter((client) => {\n        return client.id !== clientId\n      })\n\n      // Unregister itself when there are no more clients\n      if (remainingClients.length === 0) {\n        self.registration.unregister()\n      }\n\n      break\n    }\n  }\n})\n\nself.addEventListener('fetch', function (event) {\n  const { request } = event\n\n  // Bypass navigation requests.\n  if (request.mode === 'navigate') {\n    return\n  }\n\n  // Opening the DevTools triggers the \"only-if-cached\" request\n  // that cannot be handled by the worker. Bypass such requests.\n  if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {\n    return\n  }\n\n  // Bypass all requests when there are no active clients.\n  // Prevents the self-unregistered worked from handling requests\n  // after it's been deleted (still remains active until the next reload).\n  if (activeClientIds.size === 0) {\n    return\n  }\n\n  // Generate unique request ID.\n  const requestId = crypto.randomUUID()\n  event.respondWith(handleRequest(event, requestId))\n})\n\nasync function handleRequest(event, requestId) {\n  const client = await resolveMainClient(event)\n  const response = await getResponse(event, client, requestId)\n\n  // Send back the response clone for the \"response:*\" life-cycle events.\n  // Ensure MSW is active and ready to handle the message, otherwise\n  // this message will pend indefinitely.\n  if (client && activeClientIds.has(client.id)) {\n    ;(async function () {\n      const responseClone = response.clone()\n\n      sendToClient(\n        client,\n        {\n          type: 'RESPONSE',\n          payload: {\n            requestId,\n            isMockedResponse: IS_MOCKED_RESPONSE in response,\n            type: responseClone.type,\n            status: responseClone.status,\n            statusText: responseClone.statusText,\n            body: responseClone.body,\n            headers: Object.fromEntries(responseClone.headers.entries()),\n          },\n        },\n        [responseClone.body],\n      )\n    })()\n  }\n\n  return response\n}\n\n// Resolve the main client for the given event.\n// Client that issues a request doesn't necessarily equal the client\n// that registered the worker. It's with the latter the worker should\n// communicate with during the response resolving phase.\nasync function resolveMainClient(event) {\n  const client = await self.clients.get(event.clientId)\n\n  if (client?.frameType === 'top-level') {\n    return client\n  }\n\n  const allClients = await self.clients.matchAll({\n    type: 'window',\n  })\n\n  return allClients\n    .filter((client) => {\n      // Get only those clients that are currently visible.\n      return client.visibilityState === 'visible'\n    })\n    .find((client) => {\n      // Find the client ID that's recorded in the\n      // set of clients that have registered the worker.\n      return activeClientIds.has(client.id)\n    })\n}\n\nasync function getResponse(event, client, requestId) {\n  const { request } = event\n\n  // Clone the request because it might've been already used\n  // (i.e. its body has been read and sent to the client).\n  const requestClone = request.clone()\n\n  function passthrough() {\n    const headers = Object.fromEntries(requestClone.headers.entries())\n\n    // Remove internal MSW request header so the passthrough request\n    // complies with any potential CORS preflight checks on the server.\n    // Some servers forbid unknown request headers.\n    delete headers['x-msw-intention']\n\n    return fetch(requestClone, { headers })\n  }\n\n  // Bypass mocking when the client is not active.\n  if (!client) {\n    return passthrough()\n  }\n\n  // Bypass initial page load requests (i.e. static assets).\n  // The absence of the immediate/parent client in the map of the active clients\n  // means that MSW hasn't dispatched the \"MOCK_ACTIVATE\" event yet\n  // and is not ready to handle requests.\n  if (!activeClientIds.has(client.id)) {\n    return passthrough()\n  }\n\n  // Notify the client that a request has been intercepted.\n  const requestBuffer = await request.arrayBuffer()\n  const clientMessage = await sendToClient(\n    client,\n    {\n      type: 'REQUEST',\n      payload: {\n        id: requestId,\n        url: request.url,\n        mode: request.mode,\n        method: request.method,\n        headers: Object.fromEntries(request.headers.entries()),\n        cache: request.cache,\n        credentials: request.credentials,\n        destination: request.destination,\n        integrity: request.integrity,\n        redirect: request.redirect,\n        referrer: request.referrer,\n        referrerPolicy: request.referrerPolicy,\n        body: requestBuffer,\n        keepalive: request.keepalive,\n      },\n    },\n    [requestBuffer],\n  )\n\n  switch (clientMessage.type) {\n    case 'MOCK_RESPONSE': {\n      return respondWithMock(clientMessage.data)\n    }\n\n    case 'PASSTHROUGH': {\n      return passthrough()\n    }\n  }\n\n  return passthrough()\n}\n\nfunction sendToClient(client, message, transferrables = []) {\n  return new Promise((resolve, reject) => {\n    const channel = new MessageChannel()\n\n    channel.port1.onmessage = (event) => {\n      if (event.data && event.data.error) {\n        return reject(event.data.error)\n      }\n\n      resolve(event.data)\n    }\n\n    client.postMessage(\n      message,\n      [channel.port2].concat(transferrables.filter(Boolean)),\n    )\n  })\n}\n\nasync function respondWithMock(response) {\n  // Setting response status code to 0 is a no-op.\n  // However, when responding with a \"Response.error()\", the produced Response\n  // instance will have status code set to 0. Since it's not possible to create\n  // a Response instance with status code 0, handle that use-case separately.\n  if (response.status === 0) {\n    return Response.error()\n  }\n\n  const mockedResponse = new Response(response.body, response)\n\n  Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, {\n    value: true,\n    enumerable: true,\n  })\n\n  return mockedResponse\n}\n"
  },
  {
    "path": "apps/react-vite/public/robots.txt",
    "content": "# https://www.robotstxt.org/robotstxt.html\nUser-agent: *\nDisallow:\n"
  },
  {
    "path": "apps/react-vite/src/app/index.tsx",
    "content": "import { AppProvider } from './provider';\nimport { AppRouter } from './router';\n\nexport const App = () => {\n  return (\n    <AppProvider>\n      <AppRouter />\n    </AppProvider>\n  );\n};\n"
  },
  {
    "path": "apps/react-vite/src/app/provider.tsx",
    "content": "import { QueryClient, QueryClientProvider } from '@tanstack/react-query';\nimport { ReactQueryDevtools } from '@tanstack/react-query-devtools';\nimport * as React from 'react';\nimport { ErrorBoundary } from 'react-error-boundary';\nimport { HelmetProvider } from 'react-helmet-async';\n\nimport { MainErrorFallback } from '@/components/errors/main';\nimport { Notifications } from '@/components/ui/notifications';\nimport { Spinner } from '@/components/ui/spinner';\nimport { AuthLoader } from '@/lib/auth';\nimport { queryConfig } from '@/lib/react-query';\n\ntype AppProviderProps = {\n  children: React.ReactNode;\n};\n\nexport const AppProvider = ({ children }: AppProviderProps) => {\n  const [queryClient] = React.useState(\n    () =>\n      new QueryClient({\n        defaultOptions: queryConfig,\n      }),\n  );\n\n  return (\n    <React.Suspense\n      fallback={\n        <div className=\"flex h-screen w-screen items-center justify-center\">\n          <Spinner size=\"xl\" />\n        </div>\n      }\n    >\n      <ErrorBoundary FallbackComponent={MainErrorFallback}>\n        <HelmetProvider>\n          <QueryClientProvider client={queryClient}>\n            {import.meta.env.DEV && <ReactQueryDevtools />}\n            <Notifications />\n            <AuthLoader\n              renderLoading={() => (\n                <div className=\"flex h-screen w-screen items-center justify-center\">\n                  <Spinner size=\"xl\" />\n                </div>\n              )}\n            >\n              {children}\n            </AuthLoader>\n          </QueryClientProvider>\n        </HelmetProvider>\n      </ErrorBoundary>\n    </React.Suspense>\n  );\n};\n"
  },
  {
    "path": "apps/react-vite/src/app/router.tsx",
    "content": "import { QueryClient, useQueryClient } from '@tanstack/react-query';\nimport { useMemo } from 'react';\nimport { createBrowserRouter } from 'react-router';\nimport { RouterProvider } from 'react-router/dom';\n\nimport { paths } from '@/config/paths';\nimport { ProtectedRoute } from '@/lib/auth';\n\nimport {\n  default as AppRoot,\n  ErrorBoundary as AppRootErrorBoundary,\n} from './routes/app/root';\n\nconst convert = (queryClient: QueryClient) => (m: any) => {\n  const { clientLoader, clientAction, default: Component, ...rest } = m;\n  return {\n    ...rest,\n    loader: clientLoader?.(queryClient),\n    action: clientAction?.(queryClient),\n    Component,\n  };\n};\n\nexport const createAppRouter = (queryClient: QueryClient) =>\n  createBrowserRouter([\n    {\n      path: paths.home.path,\n      lazy: () => import('./routes/landing').then(convert(queryClient)),\n    },\n    {\n      path: paths.auth.register.path,\n      lazy: () => import('./routes/auth/register').then(convert(queryClient)),\n    },\n    {\n      path: paths.auth.login.path,\n      lazy: () => import('./routes/auth/login').then(convert(queryClient)),\n    },\n    {\n      path: paths.app.root.path,\n      element: (\n        <ProtectedRoute>\n          <AppRoot />\n        </ProtectedRoute>\n      ),\n      ErrorBoundary: AppRootErrorBoundary,\n      children: [\n        {\n          path: paths.app.discussions.path,\n          lazy: () =>\n            import('./routes/app/discussions/discussions').then(\n              convert(queryClient),\n            ),\n        },\n        {\n          path: paths.app.discussion.path,\n          lazy: () =>\n            import('./routes/app/discussions/discussion').then(\n              convert(queryClient),\n            ),\n        },\n        {\n          path: paths.app.users.path,\n          lazy: () => import('./routes/app/users').then(convert(queryClient)),\n        },\n        {\n          path: paths.app.profile.path,\n          lazy: () => import('./routes/app/profile').then(convert(queryClient)),\n        },\n        {\n          path: paths.app.dashboard.path,\n          lazy: () =>\n            import('./routes/app/dashboard').then(convert(queryClient)),\n        },\n      ],\n    },\n    {\n      path: '*',\n      lazy: () => import('./routes/not-found').then(convert(queryClient)),\n    },\n  ]);\n\nexport const AppRouter = () => {\n  const queryClient = useQueryClient();\n\n  const router = useMemo(() => createAppRouter(queryClient), [queryClient]);\n\n  return <RouterProvider router={router} />;\n};\n"
  },
  {
    "path": "apps/react-vite/src/app/routes/app/dashboard.tsx",
    "content": "import { ContentLayout } from '@/components/layouts';\nimport { useUser } from '@/lib/auth';\nimport { ROLES } from '@/lib/authorization';\n\nconst DashboardRoute = () => {\n  const user = useUser();\n  return (\n    <ContentLayout title=\"Dashboard\">\n      <h1 className=\"text-xl\">\n        Welcome <b>{`${user.data?.firstName} ${user.data?.lastName}`}</b>\n      </h1>\n      <h4 className=\"my-3\">\n        Your role is : <b>{user.data?.role}</b>\n      </h4>\n      <p className=\"font-medium\">In this application you can:</p>\n      {user.data?.role === ROLES.USER && (\n        <ul className=\"my-4 list-inside list-disc\">\n          <li>Create comments in discussions</li>\n          <li>Delete own comments</li>\n        </ul>\n      )}\n      {user.data?.role === ROLES.ADMIN && (\n        <ul className=\"my-4 list-inside list-disc\">\n          <li>Create discussions</li>\n          <li>Edit discussions</li>\n          <li>Delete discussions</li>\n          <li>Comment on discussions</li>\n          <li>Delete all comments</li>\n        </ul>\n      )}\n    </ContentLayout>\n  );\n};\n\nexport default DashboardRoute;\n"
  },
  {
    "path": "apps/react-vite/src/app/routes/app/discussions/__tests__/discussion.test.tsx",
    "content": "import {\n  renderApp,\n  screen,\n  userEvent,\n  waitFor,\n  createDiscussion,\n  createUser,\n  within,\n} from '@/testing/test-utils';\n\nimport { default as DiscussionRoute } from '../discussion';\n\nconst renderDiscussion = async () => {\n  const fakeUser = await createUser();\n  const fakeDiscussion = await createDiscussion({ teamId: fakeUser.teamId });\n\n  const utils = await renderApp(<DiscussionRoute />, {\n    user: fakeUser,\n    path: `/app/discussions/:discussionId`,\n    url: `/app/discussions/${fakeDiscussion.id}`,\n  });\n\n  await screen.findByText(fakeDiscussion.title);\n\n  return {\n    ...utils,\n    fakeUser,\n    fakeDiscussion,\n  };\n};\n\ntest('should render discussion', async () => {\n  const { fakeDiscussion } = await renderDiscussion();\n  expect(screen.getByText(fakeDiscussion.body)).toBeInTheDocument();\n});\n\ntest('should update discussion', async () => {\n  const { fakeDiscussion } = await renderDiscussion();\n\n  const titleUpdate = '-Updated';\n  const bodyUpdate = '-Updated';\n\n  await userEvent.click(\n    screen.getByRole('button', { name: /update discussion/i }),\n  );\n\n  const drawer = await screen.findByRole('dialog', {\n    name: /update discussion/i,\n  });\n\n  const titleField = within(drawer).getByText(/title/i);\n  const bodyField = within(drawer).getByText(/body/i);\n\n  const newTitle = `${fakeDiscussion.title}${titleUpdate}`;\n  const newBody = `${fakeDiscussion.body}${bodyUpdate}`;\n\n  // replacing the title with the new title\n  await userEvent.type(titleField, newTitle);\n\n  // appending updated to the body\n  await userEvent.type(bodyField, bodyUpdate);\n\n  const submitButton = within(drawer).getByRole('button', {\n    name: /submit/i,\n  });\n\n  await userEvent.click(submitButton);\n\n  await waitFor(() => expect(drawer).not.toBeInTheDocument());\n\n  expect(\n    await screen.findByRole('heading', { name: newTitle }),\n  ).toBeInTheDocument();\n  expect(await screen.findByText(newBody)).toBeInTheDocument();\n});\n\ntest(\n  'should create and delete a comment on the discussion',\n  async () => {\n    await renderDiscussion();\n\n    const comment = 'Hello World';\n\n    await userEvent.click(\n      screen.getByRole('button', { name: /create comment/i }),\n    );\n\n    const drawer = await screen.findByRole('dialog', {\n      name: /create comment/i,\n    });\n\n    const bodyField = await within(drawer).findByText(/body/i);\n\n    await userEvent.type(bodyField, comment);\n\n    const submitButton = await within(drawer).findByRole('button', {\n      name: /submit/i,\n    });\n\n    await userEvent.click(submitButton);\n\n    await waitFor(() => expect(drawer).not.toBeInTheDocument());\n\n    await screen.findByText(comment);\n\n    const commentsList = await screen.findByRole('list', {\n      name: 'comments',\n    });\n\n    const commentElements =\n      await within(commentsList).findAllByRole('listitem');\n\n    const commentElement = commentElements[0];\n\n    expect(commentElement).toBeInTheDocument();\n\n    const deleteCommentButton = within(commentElement).getByRole('button', {\n      name: /delete comment/i,\n      // exact: false,\n    });\n\n    await userEvent.click(deleteCommentButton);\n\n    const confirmationDialog = await screen.findByRole('dialog', {\n      name: /delete comment/i,\n    });\n\n    const confirmationDeleteButton = await within(\n      confirmationDialog,\n    ).findByRole('button', {\n      name: /delete/i,\n    });\n\n    await userEvent.click(confirmationDeleteButton);\n\n    await screen.findByText(/comment deleted/i);\n\n    await waitFor(() => {\n      expect(within(commentsList).queryByText(comment)).not.toBeInTheDocument();\n    });\n  },\n  {\n    timeout: 20000,\n  },\n);\n"
  },
  {
    "path": "apps/react-vite/src/app/routes/app/discussions/__tests__/discussions.test.tsx",
    "content": "import type { Mock } from 'vitest';\n\nimport { createDiscussion } from '@/testing/data-generators';\nimport {\n  renderApp,\n  screen,\n  userEvent,\n  waitFor,\n  within,\n} from '@/testing/test-utils';\nimport { formatDate } from '@/utils/format';\n\nimport { default as DiscussionsRoute } from '../discussions';\n\nbeforeAll(() => {\n  vi.spyOn(console, 'error').mockImplementation(() => {});\n});\n\nafterAll(() => {\n  (console.error as Mock).mockRestore();\n});\n\ntest(\n  'should create, render and delete discussions',\n  { timeout: 10000 },\n  async () => {\n    await renderApp(<DiscussionsRoute />);\n\n    const newDiscussion = createDiscussion();\n\n    expect(await screen.findByText(/no entries/i)).toBeInTheDocument();\n\n    await userEvent.click(\n      screen.getByRole('button', { name: /create discussion/i }),\n    );\n\n    const drawer = await screen.findByRole('dialog', {\n      name: /create discussion/i,\n    });\n\n    const titleField = within(drawer).getByText(/title/i);\n    const bodyField = within(drawer).getByText(/body/i);\n\n    await userEvent.type(titleField, newDiscussion.title);\n    await userEvent.type(bodyField, newDiscussion.body);\n\n    const submitButton = within(drawer).getByRole('button', {\n      name: /submit/i,\n    });\n\n    await userEvent.click(submitButton);\n\n    await waitFor(() => expect(drawer).not.toBeInTheDocument());\n\n    const row = await screen.findByRole(\n      'row',\n      {\n        name: `${newDiscussion.title} ${formatDate(newDiscussion.createdAt)} View Delete Discussion`,\n      },\n      { timeout: 5000 },\n    );\n\n    expect(\n      within(row).getByRole('cell', {\n        name: newDiscussion.title,\n      }),\n    ).toBeInTheDocument();\n\n    await userEvent.click(\n      within(row).getByRole('button', {\n        name: /delete discussion/i,\n      }),\n    );\n\n    const confirmationDialog = await screen.findByRole('dialog', {\n      name: /delete discussion/i,\n    });\n\n    const confirmationDeleteButton = within(confirmationDialog).getByRole(\n      'button',\n      {\n        name: /delete discussion/i,\n      },\n    );\n\n    await userEvent.click(confirmationDeleteButton);\n\n    await screen.findByText(/discussion deleted/i);\n\n    expect(\n      within(row).queryByRole('cell', {\n        name: newDiscussion.title,\n      }),\n    ).not.toBeInTheDocument();\n  },\n);\n"
  },
  {
    "path": "apps/react-vite/src/app/routes/app/discussions/discussion.tsx",
    "content": "import { QueryClient } from '@tanstack/react-query';\nimport { ErrorBoundary } from 'react-error-boundary';\nimport { useParams, LoaderFunctionArgs } from 'react-router';\n\nimport { ContentLayout } from '@/components/layouts';\nimport { Spinner } from '@/components/ui/spinner';\nimport { getInfiniteCommentsQueryOptions } from '@/features/comments/api/get-comments';\nimport { Comments } from '@/features/comments/components/comments';\nimport {\n  useDiscussion,\n  getDiscussionQueryOptions,\n} from '@/features/discussions/api/get-discussion';\nimport { DiscussionView } from '@/features/discussions/components/discussion-view';\n\nexport const clientLoader =\n  (queryClient: QueryClient) =>\n  async ({ params }: LoaderFunctionArgs) => {\n    const discussionId = params.discussionId as string;\n\n    const discussionQuery = getDiscussionQueryOptions(discussionId);\n    const commentsQuery = getInfiniteCommentsQueryOptions(discussionId);\n\n    const promises = [\n      queryClient.getQueryData(discussionQuery.queryKey) ??\n        (await queryClient.fetchQuery(discussionQuery)),\n      queryClient.getQueryData(commentsQuery.queryKey) ??\n        (await queryClient.fetchInfiniteQuery(commentsQuery)),\n    ] as const;\n\n    const [discussion, comments] = await Promise.all(promises);\n\n    return {\n      discussion,\n      comments,\n    };\n  };\n\nconst DiscussionRoute = () => {\n  const params = useParams();\n  const discussionId = params.discussionId as string;\n  const discussionQuery = useDiscussion({\n    discussionId,\n  });\n\n  if (discussionQuery.isLoading) {\n    return (\n      <div className=\"flex h-48 w-full items-center justify-center\">\n        <Spinner size=\"lg\" />\n      </div>\n    );\n  }\n\n  const discussion = discussionQuery.data?.data;\n\n  if (!discussion) return null;\n\n  return (\n    <>\n      <ContentLayout title={discussion.title}>\n        <DiscussionView discussionId={discussionId} />\n        <div className=\"mt-8\">\n          <ErrorBoundary\n            fallback={\n              <div>Failed to load comments. Try to refresh the page.</div>\n            }\n          >\n            <Comments discussionId={discussionId} />\n          </ErrorBoundary>\n        </div>\n      </ContentLayout>\n    </>\n  );\n};\n\nexport default DiscussionRoute;\n"
  },
  {
    "path": "apps/react-vite/src/app/routes/app/discussions/discussions.tsx",
    "content": "import { QueryClient, useQueryClient } from '@tanstack/react-query';\nimport { LoaderFunctionArgs } from 'react-router';\n\nimport { ContentLayout } from '@/components/layouts';\nimport { getInfiniteCommentsQueryOptions } from '@/features/comments/api/get-comments';\nimport { getDiscussionsQueryOptions } from '@/features/discussions/api/get-discussions';\nimport { CreateDiscussion } from '@/features/discussions/components/create-discussion';\nimport { DiscussionsList } from '@/features/discussions/components/discussions-list';\n\nexport const clientLoader =\n  (queryClient: QueryClient) =>\n  async ({ request }: LoaderFunctionArgs) => {\n    const url = new URL(request.url);\n\n    const page = Number(url.searchParams.get('page') || 1);\n\n    const query = getDiscussionsQueryOptions({ page });\n\n    return (\n      queryClient.getQueryData(query.queryKey) ??\n      (await queryClient.fetchQuery(query))\n    );\n  };\n\nconst DiscussionsRoute = () => {\n  const queryClient = useQueryClient();\n  return (\n    <ContentLayout title=\"Discussions\">\n      <div className=\"flex justify-end\">\n        <CreateDiscussion />\n      </div>\n      <div className=\"mt-4\">\n        <DiscussionsList\n          onDiscussionPrefetch={(id) => {\n            // Prefetch the comments data when the user hovers over the link in the list\n            queryClient.prefetchInfiniteQuery(\n              getInfiniteCommentsQueryOptions(id),\n            );\n          }}\n        />\n      </div>\n    </ContentLayout>\n  );\n};\n\nexport default DiscussionsRoute;\n"
  },
  {
    "path": "apps/react-vite/src/app/routes/app/profile.tsx",
    "content": "import { ContentLayout } from '@/components/layouts';\nimport { UpdateProfile } from '@/features/users/components/update-profile';\nimport { useUser } from '@/lib/auth';\n\ntype EntryProps = {\n  label: string;\n  value: string;\n};\nconst Entry = ({ label, value }: EntryProps) => (\n  <div className=\"py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5\">\n    <dt className=\"text-sm font-medium text-gray-500\">{label}</dt>\n    <dd className=\"mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0\">\n      {value}\n    </dd>\n  </div>\n);\n\nconst ProfileRoute = () => {\n  const user = useUser();\n\n  if (!user.data) return null;\n\n  return (\n    <ContentLayout title=\"Profile\">\n      <div className=\"overflow-hidden bg-white shadow sm:rounded-lg\">\n        <div className=\"px-4 py-5 sm:px-6\">\n          <div className=\"flex justify-between\">\n            <h3 className=\"text-lg font-medium leading-6 text-gray-900\">\n              User Information\n            </h3>\n            <UpdateProfile />\n          </div>\n          <p className=\"mt-1 max-w-2xl text-sm text-gray-500\">\n            Personal details of the user.\n          </p>\n        </div>\n        <div className=\"border-t border-gray-200 px-4 py-5 sm:p-0\">\n          <dl className=\"sm:divide-y sm:divide-gray-200\">\n            <Entry label=\"First Name\" value={user.data.firstName} />\n            <Entry label=\"Last Name\" value={user.data.lastName} />\n            <Entry label=\"Email Address\" value={user.data.email} />\n            <Entry label=\"Role\" value={user.data.role} />\n            <Entry label=\"Bio\" value={user.data.bio} />\n          </dl>\n        </div>\n      </div>\n    </ContentLayout>\n  );\n};\n\nexport default ProfileRoute;\n"
  },
  {
    "path": "apps/react-vite/src/app/routes/app/root.tsx",
    "content": "import { Outlet } from 'react-router';\n\nimport { DashboardLayout } from '@/components/layouts';\n\nexport const ErrorBoundary = () => {\n  return <div>Something went wrong!</div>;\n};\n\nconst AppRoot = () => {\n  return (\n    <DashboardLayout>\n      <Outlet />\n    </DashboardLayout>\n  );\n};\n\nexport default AppRoot;\n"
  },
  {
    "path": "apps/react-vite/src/app/routes/app/users.tsx",
    "content": "import { QueryClient } from '@tanstack/react-query';\n\nimport { ContentLayout } from '@/components/layouts';\nimport { getUsersQueryOptions } from '@/features/users/api/get-users';\nimport { UsersList } from '@/features/users/components/users-list';\nimport { Authorization, ROLES } from '@/lib/authorization';\n\nexport const clientLoader = (queryClient: QueryClient) => async () => {\n  const query = getUsersQueryOptions();\n\n  return (\n    queryClient.getQueryData(query.queryKey) ??\n    (await queryClient.fetchQuery(query))\n  );\n};\n\nconst UsersRoute = () => {\n  return (\n    <ContentLayout title=\"Users\">\n      <Authorization\n        forbiddenFallback={<div>Only admin can view this.</div>}\n        allowedRoles={[ROLES.ADMIN]}\n      >\n        <UsersList />\n      </Authorization>\n    </ContentLayout>\n  );\n};\n\nexport default UsersRoute;\n"
  },
  {
    "path": "apps/react-vite/src/app/routes/auth/login.tsx",
    "content": "import { useNavigate, useSearchParams } from 'react-router';\n\nimport { AuthLayout } from '@/components/layouts/auth-layout';\nimport { paths } from '@/config/paths';\nimport { LoginForm } from '@/features/auth/components/login-form';\n\nconst LoginRoute = () => {\n  const navigate = useNavigate();\n  const [searchParams] = useSearchParams();\n  const redirectTo = searchParams.get('redirectTo');\n\n  return (\n    <AuthLayout title=\"Log in to your account\">\n      <LoginForm\n        onSuccess={() => {\n          navigate(\n            `${redirectTo ? `${redirectTo}` : paths.app.dashboard.getHref()}`,\n            {\n              replace: true,\n            },\n          );\n        }}\n      />\n    </AuthLayout>\n  );\n};\n\nexport default LoginRoute;\n"
  },
  {
    "path": "apps/react-vite/src/app/routes/auth/register.tsx",
    "content": "import { useState } from 'react';\nimport { useNavigate, useSearchParams } from 'react-router';\n\nimport { AuthLayout } from '@/components/layouts/auth-layout';\nimport { paths } from '@/config/paths';\nimport { RegisterForm } from '@/features/auth/components/register-form';\nimport { useTeams } from '@/features/teams/api/get-teams';\n\nconst RegisterRoute = () => {\n  const navigate = useNavigate();\n  const [searchParams] = useSearchParams();\n  const redirectTo = searchParams.get('redirectTo');\n  const [chooseTeam, setChooseTeam] = useState(false);\n\n  const teamsQuery = useTeams({\n    queryConfig: {\n      enabled: chooseTeam,\n    },\n  });\n\n  return (\n    <AuthLayout title=\"Register your account\">\n      <RegisterForm\n        onSuccess={() => {\n          navigate(\n            `${redirectTo ? `${redirectTo}` : paths.app.dashboard.getHref()}`,\n            {\n              replace: true,\n            },\n          );\n        }}\n        chooseTeam={chooseTeam}\n        setChooseTeam={() => setChooseTeam(!chooseTeam)}\n        teams={teamsQuery.data?.data}\n      />\n    </AuthLayout>\n  );\n};\n\nexport default RegisterRoute;\n"
  },
  {
    "path": "apps/react-vite/src/app/routes/landing.tsx",
    "content": "import { useNavigate } from 'react-router';\n\nimport logo from '@/assets/logo.svg';\nimport { Head } from '@/components/seo';\nimport { Button } from '@/components/ui/button';\nimport { paths } from '@/config/paths';\nimport { useUser } from '@/lib/auth';\n\nconst LandingRoute = () => {\n  const navigate = useNavigate();\n  const user = useUser();\n\n  const handleStart = () => {\n    if (user.data) {\n      navigate(paths.app.dashboard.getHref());\n    } else {\n      navigate(paths.auth.login.getHref());\n    }\n  };\n\n  return (\n    <>\n      <Head description=\"Welcome to bulletproof react\" />\n      <div className=\"flex h-screen items-center bg-white\">\n        <div className=\"mx-auto max-w-7xl px-4 py-12 text-center sm:px-6 lg:px-8 lg:py-16\">\n          <h2 className=\"text-3xl font-extrabold tracking-tight text-gray-900 sm:text-4xl\">\n            <span className=\"block\">Bulletproof React</span>\n          </h2>\n          <img src={logo} alt=\"react\" />\n          <p>Showcasing Best Practices For Building React Applications</p>\n          <div className=\"mt-8 flex justify-center\">\n            <div className=\"inline-flex rounded-md shadow\">\n              <Button\n                onClick={handleStart}\n                icon={\n                  <svg\n                    xmlns=\"http://www.w3.org/2000/svg\"\n                    className=\"size-6\"\n                    fill=\"none\"\n                    viewBox=\"0 0 24 24\"\n                    stroke=\"currentColor\"\n                  >\n                    <path\n                      strokeLinecap=\"round\"\n                      strokeLinejoin=\"round\"\n                      strokeWidth=\"2\"\n                      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\"\n                    />\n                  </svg>\n                }\n              >\n                Get started\n              </Button>\n            </div>\n            <div className=\"ml-3 inline-flex\">\n              <a\n                href=\"https://github.com/alan2207/bulletproof-react\"\n                target=\"_blank\"\n                rel=\"noreferrer\"\n              >\n                <Button\n                  variant=\"outline\"\n                  icon={\n                    <svg\n                      fill=\"currentColor\"\n                      viewBox=\"0 0 24 24\"\n                      className=\"size-6\"\n                    >\n                      <path\n                        fillRule=\"evenodd\"\n                        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\"\n                        clipRule=\"evenodd\"\n                      />\n                    </svg>\n                  }\n                >\n                  Github Repo\n                </Button>\n              </a>\n            </div>\n          </div>\n        </div>\n      </div>\n    </>\n  );\n};\n\nexport default LandingRoute;\n"
  },
  {
    "path": "apps/react-vite/src/app/routes/not-found.tsx",
    "content": "import { Link } from '@/components/ui/link';\nimport { paths } from '@/config/paths';\n\nconst NotFoundRoute = () => {\n  return (\n    <div className=\"mt-52 flex flex-col items-center font-semibold\">\n      <h1>404 - Not Found</h1>\n      <p>Sorry, the page you are looking for does not exist.</p>\n      <Link to={paths.home.getHref()} replace>\n        Go to Home\n      </Link>\n    </div>\n  );\n};\n\nexport default NotFoundRoute;\n"
  },
  {
    "path": "apps/react-vite/src/components/errors/main.tsx",
    "content": "import { Button } from '../ui/button';\n\nexport const MainErrorFallback = () => {\n  return (\n    <div\n      className=\"flex h-screen w-screen flex-col items-center justify-center text-red-500\"\n      role=\"alert\"\n    >\n      <h2 className=\"text-lg font-semibold\">Ooops, something went wrong :( </h2>\n      <Button\n        className=\"mt-4\"\n        onClick={() => window.location.assign(window.location.origin)}\n      >\n        Refresh\n      </Button>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/react-vite/src/components/layouts/auth-layout.tsx",
    "content": "import * as React from 'react';\nimport { useEffect } from 'react';\nimport { useNavigate, useSearchParams } from 'react-router';\n\nimport logo from '@/assets/logo.svg';\nimport { Head } from '@/components/seo';\nimport { Link } from '@/components/ui/link';\nimport { paths } from '@/config/paths';\nimport { useUser } from '@/lib/auth';\n\ntype LayoutProps = {\n  children: React.ReactNode;\n  title: string;\n};\n\nexport const AuthLayout = ({ children, title }: LayoutProps) => {\n  const user = useUser();\n  const [searchParams] = useSearchParams();\n  const redirectTo = searchParams.get('redirectTo');\n\n  const navigate = useNavigate();\n\n  useEffect(() => {\n    if (user.data) {\n      navigate(redirectTo ? redirectTo : paths.app.dashboard.getHref(), {\n        replace: true,\n      });\n    }\n  }, [user.data, navigate, redirectTo]);\n\n  return (\n    <>\n      <Head title={title} />\n      <div className=\"flex min-h-screen flex-col justify-center bg-gray-50 py-12 sm:px-6 lg:px-8\">\n        <div className=\"sm:mx-auto sm:w-full sm:max-w-md\">\n          <div className=\"flex justify-center\">\n            <Link\n              className=\"flex items-center text-white\"\n              to={paths.home.getHref()}\n            >\n              <img className=\"h-24 w-auto\" src={logo} alt=\"Workflow\" />\n            </Link>\n          </div>\n\n          <h2 className=\"mt-3 text-center text-3xl font-extrabold text-gray-900\">\n            {title}\n          </h2>\n        </div>\n\n        <div className=\"mt-8 sm:mx-auto sm:w-full sm:max-w-md\">\n          <div className=\"bg-white px-4 py-8 shadow sm:rounded-lg sm:px-10\">\n            {children}\n          </div>\n        </div>\n      </div>\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/react-vite/src/components/layouts/content-layout.tsx",
    "content": "import * as React from 'react';\n\nimport { Head } from '../seo';\n\ntype ContentLayoutProps = {\n  children: React.ReactNode;\n  title: string;\n};\n\nexport const ContentLayout = ({ children, title }: ContentLayoutProps) => {\n  return (\n    <>\n      <Head title={title} />\n      <div className=\"py-6\">\n        <div className=\"mx-auto max-w-7xl px-4 sm:px-6 md:px-8\">\n          <h1 className=\"text-2xl font-semibold text-gray-900\">{title}</h1>\n        </div>\n        <div className=\"mx-auto max-w-7xl px-4 py-6 sm:px-6 md:px-8\">\n          {children}\n        </div>\n      </div>\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/react-vite/src/components/layouts/dashboard-layout.tsx",
    "content": "import { Home, PanelLeft, Folder, Users, User2 } from 'lucide-react';\nimport { useEffect, useState } from 'react';\nimport { NavLink, useNavigate, useNavigation } from 'react-router';\n\nimport logo from '@/assets/logo.svg';\nimport { Button } from '@/components/ui/button';\nimport { Drawer, DrawerContent, DrawerTrigger } from '@/components/ui/drawer';\nimport { paths } from '@/config/paths';\nimport { useLogout } from '@/lib/auth';\nimport { ROLES, useAuthorization } from '@/lib/authorization';\nimport { cn } from '@/utils/cn';\n\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from '../ui/dropdown';\nimport { Link } from '../ui/link';\n\ntype SideNavigationItem = {\n  name: string;\n  to: string;\n  icon: (props: React.SVGProps<SVGSVGElement>) => JSX.Element;\n};\n\nconst Logo = () => {\n  return (\n    <Link className=\"flex items-center text-white\" to={paths.home.getHref()}>\n      <img className=\"h-8 w-auto\" src={logo} alt=\"Workflow\" />\n      <span className=\"text-sm font-semibold text-white\">\n        Bulletproof React\n      </span>\n    </Link>\n  );\n};\n\nconst Progress = () => {\n  const { state, location } = useNavigation();\n\n  const [progress, setProgress] = useState(0);\n\n  useEffect(() => {\n    setProgress(0);\n  }, [location?.pathname]);\n\n  useEffect(() => {\n    if (state === 'loading') {\n      const timer = setInterval(() => {\n        setProgress((oldProgress) => {\n          if (oldProgress === 100) {\n            clearInterval(timer);\n            return 100;\n          }\n          const newProgress = oldProgress + 10;\n          return newProgress > 100 ? 100 : newProgress;\n        });\n      }, 300);\n\n      return () => {\n        clearInterval(timer);\n      };\n    }\n  }, [state]);\n\n  if (state !== 'loading') {\n    return null;\n  }\n\n  return (\n    <div\n      className=\"fixed left-0 top-0 h-1 bg-blue-500 transition-all duration-200 ease-in-out\"\n      style={{ width: `${progress}%` }}\n    ></div>\n  );\n};\n\nexport function DashboardLayout({ children }: { children: React.ReactNode }) {\n  const navigate = useNavigate();\n  const logout = useLogout({\n    onSuccess: () => navigate(paths.auth.login.getHref(location.pathname)),\n  });\n  const { checkAccess } = useAuthorization();\n  const navigation = [\n    { name: 'Dashboard', to: paths.app.dashboard.getHref(), icon: Home },\n    { name: 'Discussions', to: paths.app.discussions.getHref(), icon: Folder },\n    checkAccess({ allowedRoles: [ROLES.ADMIN] }) && {\n      name: 'Users',\n      to: paths.app.users.getHref(),\n      icon: Users,\n    },\n  ].filter(Boolean) as SideNavigationItem[];\n\n  return (\n    <div className=\"flex min-h-screen w-full flex-col bg-muted/40\">\n      <aside className=\"fixed inset-y-0 left-0 z-10 hidden w-60 flex-col border-r bg-black sm:flex\">\n        <nav className=\"flex flex-col items-center gap-4 px-2 py-4\">\n          <div className=\"flex h-16 shrink-0 items-center px-4\">\n            <Logo />\n          </div>\n          {navigation.map((item) => (\n            <NavLink\n              key={item.name}\n              to={item.to}\n              end={item.name !== 'Discussions'}\n              className={({ isActive }) =>\n                cn(\n                  'text-gray-300 hover:bg-gray-700 hover:text-white',\n                  'group flex flex-1 w-full items-center rounded-md p-2 text-base font-medium',\n                  isActive && 'bg-gray-900 text-white',\n                )\n              }\n            >\n              <item.icon\n                className={cn(\n                  'text-gray-400 group-hover:text-gray-300',\n                  'mr-4 size-6 shrink-0',\n                )}\n                aria-hidden=\"true\"\n              />\n              {item.name}\n            </NavLink>\n          ))}\n        </nav>\n      </aside>\n      <div className=\"flex flex-col sm:gap-4 sm:py-4 sm:pl-60\">\n        <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\">\n          <Progress />\n          <Drawer>\n            <DrawerTrigger asChild>\n              <Button size=\"icon\" variant=\"outline\" className=\"sm:hidden\">\n                <PanelLeft className=\"size-5\" />\n                <span className=\"sr-only\">Toggle Menu</span>\n              </Button>\n            </DrawerTrigger>\n            <DrawerContent\n              side=\"left\"\n              className=\"bg-black pt-10 text-white sm:max-w-60\"\n            >\n              <nav className=\"grid gap-6 text-lg font-medium\">\n                <div className=\"flex h-16 shrink-0 items-center px-4\">\n                  <Logo />\n                </div>\n                {navigation.map((item) => (\n                  <NavLink\n                    key={item.name}\n                    to={item.to}\n                    end\n                    className={({ isActive }) =>\n                      cn(\n                        'text-gray-300 hover:bg-gray-700 hover:text-white',\n                        'group flex flex-1 w-full items-center rounded-md p-2 text-base font-medium',\n                        isActive && 'bg-gray-900 text-white',\n                      )\n                    }\n                  >\n                    <item.icon\n                      className={cn(\n                        'text-gray-400 group-hover:text-gray-300',\n                        'mr-4 size-6 shrink-0',\n                      )}\n                      aria-hidden=\"true\"\n                    />\n                    {item.name}\n                  </NavLink>\n                ))}\n              </nav>\n            </DrawerContent>\n          </Drawer>\n          <DropdownMenu>\n            <DropdownMenuTrigger asChild>\n              <Button\n                variant=\"outline\"\n                size=\"icon\"\n                className=\"overflow-hidden rounded-full\"\n              >\n                <span className=\"sr-only\">Open user menu</span>\n                <User2 className=\"size-6 rounded-full\" />\n              </Button>\n            </DropdownMenuTrigger>\n            <DropdownMenuContent align=\"end\">\n              <DropdownMenuItem\n                onClick={() => navigate(paths.app.profile.getHref())}\n                className={cn('block px-4 py-2 text-sm text-gray-700')}\n              >\n                Your Profile\n              </DropdownMenuItem>\n              <DropdownMenuSeparator />\n              <DropdownMenuItem\n                className={cn('block px-4 py-2 text-sm text-gray-700 w-full')}\n                onClick={() => logout.mutate({})}\n              >\n                Sign Out\n              </DropdownMenuItem>\n            </DropdownMenuContent>\n          </DropdownMenu>\n        </header>\n        <main className=\"grid flex-1 items-start gap-4 p-4 sm:px-6 sm:py-0 md:gap-8\">\n          {children}\n        </main>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/react-vite/src/components/layouts/index.ts",
    "content": "export * from './content-layout';\nexport * from './dashboard-layout';\n"
  },
  {
    "path": "apps/react-vite/src/components/seo/__tests__/head.test.tsx",
    "content": "import { render, waitFor } from '@/testing/test-utils';\n\nimport { Head } from '../head';\n\ntest('should add proper page title and meta description', async () => {\n  const title = 'Hello World';\n  const titleSuffix = ' | Bulletproof React';\n  const description = 'This is a description';\n\n  render(<Head title={title} description={description} />);\n  await waitFor(() => expect(document.title).toEqual(title + titleSuffix));\n\n  const metaDescription = document.querySelector(\"meta[name='description']\");\n\n  expect(metaDescription?.getAttribute('content')).toEqual(description);\n});\n"
  },
  {
    "path": "apps/react-vite/src/components/seo/head.tsx",
    "content": "import { Helmet, HelmetData } from 'react-helmet-async';\n\ntype HeadProps = {\n  title?: string;\n  description?: string;\n};\n\nconst helmetData = new HelmetData({});\n\nexport const Head = ({ title = '', description = '' }: HeadProps = {}) => {\n  return (\n    <Helmet\n      helmetData={helmetData}\n      title={title ? `${title} | Bulletproof React` : undefined}\n      defaultTitle=\"Bulletproof React\"\n    >\n      <meta name=\"description\" content={description} />\n    </Helmet>\n  );\n};\n"
  },
  {
    "path": "apps/react-vite/src/components/seo/index.ts",
    "content": "export * from './head';\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/button/button.stories.tsx",
    "content": "import { Meta, StoryObj } from '@storybook/react';\n\nimport { Button } from './button';\n\nconst meta: Meta<typeof Button> = {\n  component: Button,\n};\n\nexport default meta;\ntype Story = StoryObj<typeof Button>;\n\nexport const Default: Story = {\n  args: {\n    children: 'Button',\n    variant: 'default',\n  },\n};\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/button/button.tsx",
    "content": "import { Slot } from '@radix-ui/react-slot';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport * as React from 'react';\n\nimport { cn } from '@/utils/cn';\n\nimport { Spinner } from '../spinner';\n\nconst buttonVariants = cva(\n  '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',\n  {\n    variants: {\n      variant: {\n        default:\n          'bg-primary text-primary-foreground shadow hover:bg-primary/90',\n        destructive:\n          'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',\n        outline:\n          'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',\n        secondary:\n          'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',\n        ghost: 'hover:bg-accent hover:text-accent-foreground',\n        link: 'text-primary underline-offset-4 hover:underline',\n      },\n      size: {\n        default: 'h-9 px-4 py-2',\n        sm: 'h-8 rounded-md px-3 text-xs',\n        lg: 'h-10 rounded-md px-8',\n        icon: 'size-9',\n      },\n    },\n    defaultVariants: {\n      variant: 'default',\n      size: 'default',\n    },\n  },\n);\n\nexport type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> &\n  VariantProps<typeof buttonVariants> & {\n    asChild?: boolean;\n    isLoading?: boolean;\n    icon?: React.ReactNode;\n  };\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n  (\n    {\n      className,\n      variant,\n      size,\n      asChild = false,\n      children,\n      isLoading,\n      icon,\n      ...props\n    },\n    ref,\n  ) => {\n    const Comp = asChild ? Slot : 'button';\n    return (\n      <Comp\n        className={cn(buttonVariants({ variant, size, className }))}\n        ref={ref}\n        {...props}\n      >\n        {isLoading && <Spinner size=\"sm\" className=\"text-current\" />}\n        {!isLoading && icon && <span className=\"mr-2\">{icon}</span>}\n        <span className=\"mx-2\">{children}</span>\n      </Comp>\n    );\n  },\n);\nButton.displayName = 'Button';\n\nexport { Button, buttonVariants };\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/button/index.ts",
    "content": "export * from './button';\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/dialog/__tests__/dialog.test.tsx",
    "content": "import * as React from 'react';\n\nimport { Button } from '@/components/ui/button';\nimport { useDisclosure } from '@/hooks/use-disclosure';\nimport { rtlRender, screen, userEvent, waitFor } from '@/testing/test-utils';\n\nimport {\n  Dialog,\n  DialogContent,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from '../dialog';\n\nconst openButtonText = 'Open Modal';\nconst cancelButtonText = 'Cancel';\nconst titleText = 'Modal Title';\n\nconst TestDialog = () => {\n  const { close, open, isOpen } = useDisclosure();\n  const cancelButtonRef = React.useRef(null);\n\n  return (\n    <Dialog\n      open={isOpen}\n      onOpenChange={(isOpen) => {\n        if (!isOpen) {\n          close();\n        } else {\n          open();\n        }\n      }}\n    >\n      <DialogTrigger asChild>\n        <Button variant=\"outline\">{openButtonText}</Button>\n      </DialogTrigger>\n      <DialogContent className=\"sm:max-w-[425px]\">\n        <DialogHeader>\n          <DialogTitle>{titleText}</DialogTitle>\n        </DialogHeader>\n\n        <DialogFooter>\n          <Button type=\"submit\">Submit</Button>\n          <Button ref={cancelButtonRef} variant=\"outline\" onClick={close}>\n            {cancelButtonText}\n          </Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n};\n\ntest('should handle basic dialog flow', async () => {\n  rtlRender(<TestDialog />);\n\n  expect(screen.queryByText(titleText)).not.toBeInTheDocument();\n\n  await userEvent.click(screen.getByRole('button', { name: openButtonText }));\n\n  expect(await screen.findByText(titleText)).toBeInTheDocument();\n\n  await userEvent.click(screen.getByRole('button', { name: cancelButtonText }));\n\n  await waitFor(() =>\n    expect(screen.queryByText(titleText)).not.toBeInTheDocument(),\n  );\n});\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/dialog/confirmation-dialog/__tests__/confirmation-dialog.test.tsx",
    "content": "import { Button } from '@/components/ui/button';\nimport { rtlRender, screen, userEvent, waitFor } from '@/testing/test-utils';\n\nimport { ConfirmationDialog } from '../confirmation-dialog';\n\ntest('should handle confirmation flow', async () => {\n  const titleText = 'Are you sure?';\n  const bodyText = 'Are you sure you want to delete this item?';\n  const confirmationButtonText = 'Confirm';\n  const openButtonText = 'Open';\n\n  await rtlRender(\n    <ConfirmationDialog\n      icon=\"danger\"\n      title={titleText}\n      body={bodyText}\n      confirmButton={<Button>{confirmationButtonText}</Button>}\n      triggerButton={<Button>{openButtonText}</Button>}\n    />,\n  );\n\n  expect(screen.queryByText(titleText)).not.toBeInTheDocument();\n\n  await userEvent.click(screen.getByRole('button', { name: openButtonText }));\n\n  expect(await screen.findByText(titleText)).toBeInTheDocument();\n\n  expect(screen.getByText(bodyText)).toBeInTheDocument();\n\n  await userEvent.click(screen.getByRole('button', { name: 'Cancel' }));\n\n  await waitFor(() =>\n    expect(screen.queryByText(titleText)).not.toBeInTheDocument(),\n  );\n\n  expect(screen.queryByText(bodyText)).not.toBeInTheDocument();\n});\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/dialog/confirmation-dialog/confirmation-dialog.stories.tsx",
    "content": "import { Meta, StoryObj } from '@storybook/react';\n\nimport { Button } from '@/components/ui/button';\n\nimport { ConfirmationDialog } from './confirmation-dialog';\n\nconst meta: Meta<typeof ConfirmationDialog> = {\n  component: ConfirmationDialog,\n};\n\nexport default meta;\n\ntype Story = StoryObj<typeof ConfirmationDialog>;\n\nexport const Danger: Story = {\n  args: {\n    icon: 'danger',\n    title: 'Confirmation',\n    body: 'Hello World',\n    confirmButton: <Button className=\"bg-red-500\">Confirm</Button>,\n    triggerButton: <Button>Open</Button>,\n  },\n};\n\nexport const Info: Story = {\n  args: {\n    icon: 'info',\n    title: 'Confirmation',\n    body: 'Hello World',\n    confirmButton: <Button>Confirm</Button>,\n    triggerButton: <Button>Open</Button>,\n  },\n};\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/dialog/confirmation-dialog/confirmation-dialog.tsx",
    "content": "import { CircleAlert, Info } from 'lucide-react';\nimport * as React from 'react';\nimport { useEffect } from 'react';\n\nimport { Button } from '@/components/ui/button';\nimport { useDisclosure } from '@/hooks/use-disclosure';\n\nimport {\n  Dialog,\n  DialogContent,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from '../dialog';\n\nexport type ConfirmationDialogProps = {\n  triggerButton: React.ReactElement;\n  confirmButton: React.ReactElement;\n  title: string;\n  body?: string;\n  cancelButtonText?: string;\n  icon?: 'danger' | 'info';\n  isDone?: boolean;\n};\n\nexport const ConfirmationDialog = ({\n  triggerButton,\n  confirmButton,\n  title,\n  body = '',\n  cancelButtonText = 'Cancel',\n  icon = 'danger',\n  isDone = false,\n}: ConfirmationDialogProps) => {\n  const { close, open, isOpen } = useDisclosure();\n  const cancelButtonRef = React.useRef(null);\n\n  useEffect(() => {\n    if (isDone) {\n      close();\n    }\n  }, [isDone, close]);\n\n  return (\n    <Dialog\n      open={isOpen}\n      onOpenChange={(isOpen) => {\n        if (!isOpen) {\n          close();\n        } else {\n          open();\n        }\n      }}\n    >\n      <DialogTrigger asChild>{triggerButton}</DialogTrigger>\n      <DialogContent className=\"sm:max-w-[425px]\">\n        <DialogHeader className=\"flex\">\n          <DialogTitle className=\"flex items-center gap-2\">\n            {' '}\n            {icon === 'danger' && (\n              <CircleAlert className=\"size-6 text-red-600\" aria-hidden=\"true\" />\n            )}\n            {icon === 'info' && (\n              <Info className=\"size-6 text-blue-600\" aria-hidden=\"true\" />\n            )}\n            {title}\n          </DialogTitle>\n        </DialogHeader>\n\n        <div className=\"mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left\">\n          {body && (\n            <div className=\"mt-2\">\n              <p>{body}</p>\n            </div>\n          )}\n        </div>\n\n        <DialogFooter>\n          {confirmButton}\n          <Button ref={cancelButtonRef} variant=\"outline\" onClick={close}>\n            {cancelButtonText}\n          </Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n};\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/dialog/confirmation-dialog/index.ts",
    "content": "export * from './confirmation-dialog';\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/dialog/dialog.stories.tsx",
    "content": "import { Meta, StoryObj } from '@storybook/react';\nimport * as React from 'react';\n\nimport { Button } from '@/components/ui/button';\nimport { useDisclosure } from '@/hooks/use-disclosure';\n\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from './dialog';\n\nconst DemoDialog = () => {\n  const { close, open, isOpen } = useDisclosure();\n  const cancelButtonRef = React.useRef(null);\n\n  return (\n    <Dialog\n      open={isOpen}\n      onOpenChange={(isOpen) => {\n        if (!isOpen) {\n          close();\n        } else {\n          open();\n        }\n      }}\n    >\n      <DialogTrigger asChild>\n        <Button variant=\"outline\">Open Dialog</Button>\n      </DialogTrigger>\n      <DialogContent className=\"sm:max-w-[425px]\">\n        <DialogHeader>\n          <DialogTitle>Edit profile</DialogTitle>\n          <DialogDescription>Lorem ipsum</DialogDescription>\n        </DialogHeader>\n        <div className=\"grid gap-4 py-4\">Lorem ipsum</div>\n\n        <DialogFooter>\n          <Button type=\"submit\">Save changes</Button>\n          <Button ref={cancelButtonRef} variant=\"outline\" onClick={close}>\n            Cancel\n          </Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n};\n\nconst meta: Meta = {\n  component: Dialog,\n};\n\nexport default meta;\n\ntype Story = StoryObj<typeof Dialog>;\n\nexport const Demo: Story = {\n  render: () => <DemoDialog />,\n};\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/dialog/dialog.tsx",
    "content": "import * as DialogPrimitive from '@radix-ui/react-dialog';\nimport { Cross2Icon } from '@radix-ui/react-icons';\nimport * as React from 'react';\n\nimport { cn } from '@/utils/cn';\n\nconst Dialog = DialogPrimitive.Root;\n\nconst DialogTrigger = DialogPrimitive.Trigger;\n\nconst DialogPortal = DialogPrimitive.Portal;\n\nconst DialogClose = DialogPrimitive.Close;\n\nconst DialogOverlay = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Overlay\n    ref={ref}\n    className={cn(\n      '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',\n      className,\n    )}\n    {...props}\n  />\n));\nDialogOverlay.displayName = DialogPrimitive.Overlay.displayName;\n\nconst DialogContent = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n  <DialogPortal>\n    <DialogOverlay />\n    <DialogPrimitive.Content\n      ref={ref}\n      className={cn(\n        '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',\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      <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\">\n        <Cross2Icon className=\"size-4\" />\n        <span className=\"sr-only\">Close</span>\n      </DialogPrimitive.Close>\n    </DialogPrimitive.Content>\n  </DialogPortal>\n));\nDialogContent.displayName = DialogPrimitive.Content.displayName;\n\nconst DialogHeader = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      'flex flex-col space-y-1.5 text-center sm:text-left',\n      className,\n    )}\n    {...props}\n  />\n);\nDialogHeader.displayName = 'DialogHeader';\n\nconst DialogFooter = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',\n      className,\n    )}\n    {...props}\n  />\n);\nDialogFooter.displayName = 'DialogFooter';\n\nconst DialogTitle = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Title\n    ref={ref}\n    className={cn(\n      'text-lg font-semibold leading-none tracking-tight',\n      className,\n    )}\n    {...props}\n  />\n));\nDialogTitle.displayName = DialogPrimitive.Title.displayName;\n\nconst DialogDescription = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Description\n    ref={ref}\n    className={cn('text-sm text-muted-foreground', className)}\n    {...props}\n  />\n));\nDialogDescription.displayName = DialogPrimitive.Description.displayName;\n\nexport {\n  Dialog,\n  DialogPortal,\n  DialogOverlay,\n  DialogTrigger,\n  DialogClose,\n  DialogContent,\n  DialogHeader,\n  DialogFooter,\n  DialogTitle,\n  DialogDescription,\n};\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/dialog/index.ts",
    "content": "export * from './dialog';\nexport * from './confirmation-dialog';\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/drawer/__tests__/drawer.test.tsx",
    "content": "import { Button } from '@/components/ui/button';\nimport { rtlRender, screen, userEvent, waitFor } from '@/testing/test-utils';\n\nimport {\n  Drawer,\n  DrawerClose,\n  DrawerContent,\n  DrawerFooter,\n  DrawerHeader,\n  DrawerTitle,\n  DrawerTrigger,\n} from '../drawer';\n\nconst openButtonText = 'Open Drawer';\nconst titleText = 'Drawer Title';\nconst cancelButtonText = 'Cancel';\nconst drawerContentText = 'Hello From Drawer';\n\nconst TestDrawer = () => {\n  return (\n    <Drawer>\n      <DrawerTrigger asChild>\n        <Button variant=\"outline\">{openButtonText}</Button>\n      </DrawerTrigger>\n      <DrawerContent className=\"flex max-w-[800px] flex-col justify-between sm:max-w-[540px]\">\n        <div className=\"flex flex-col\">\n          <DrawerHeader>\n            <DrawerTitle>{titleText}</DrawerTitle>\n          </DrawerHeader>\n          <div>{drawerContentText}</div>\n        </div>\n        <DrawerFooter>\n          <DrawerClose asChild>\n            <Button value=\"outline\" type=\"submit\">\n              {cancelButtonText}\n            </Button>\n          </DrawerClose>\n        </DrawerFooter>\n      </DrawerContent>\n    </Drawer>\n  );\n};\n\ntest('should handle basic drawer flow', async () => {\n  await rtlRender(<TestDrawer />);\n\n  expect(screen.queryByText(titleText)).not.toBeInTheDocument();\n\n  await userEvent.click(\n    screen.getByRole('button', {\n      name: openButtonText,\n    }),\n  );\n\n  expect(await screen.findByText(titleText)).toBeInTheDocument();\n\n  await userEvent.click(\n    screen.getByRole('button', {\n      name: cancelButtonText,\n    }),\n  );\n\n  await waitFor(() =>\n    expect(screen.queryByText(titleText)).not.toBeInTheDocument(),\n  );\n});\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/drawer/drawer.stories.tsx",
    "content": "import { Meta, StoryObj } from '@storybook/react';\n\nimport { Button } from '@/components/ui/button';\nimport { useDisclosure } from '@/hooks/use-disclosure';\n\nimport {\n  Drawer,\n  DrawerClose,\n  DrawerContent,\n  DrawerDescription,\n  DrawerFooter,\n  DrawerHeader,\n  DrawerTitle,\n  DrawerTrigger,\n} from './drawer';\n\nconst meta: Meta<typeof Drawer> = {\n  component: Drawer,\n};\n\nexport default meta;\n\ntype Story = StoryObj<typeof Drawer>;\n\nconst DemoDrawer = () => {\n  const { close, open, isOpen } = useDisclosure();\n\n  return (\n    <Drawer\n      open={isOpen}\n      onOpenChange={(isOpen) => {\n        if (!isOpen) {\n          close();\n        } else {\n          open();\n        }\n      }}\n    >\n      <DrawerTrigger asChild>\n        <Button variant=\"outline\">Open</Button>\n      </DrawerTrigger>\n      <DrawerContent className=\"flex max-w-[800px] flex-col justify-between sm:max-w-[540px]\">\n        <div className=\"flex flex-col\">\n          <DrawerHeader>\n            <DrawerTitle>Drawer Header</DrawerTitle>\n            <DrawerDescription>\n              Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n            </DrawerDescription>\n          </DrawerHeader>\n          <div>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</div>\n        </div>\n        <DrawerFooter>\n          <DrawerClose asChild>\n            <Button type=\"submit\">Save changes</Button>\n          </DrawerClose>\n        </DrawerFooter>\n      </DrawerContent>\n    </Drawer>\n  );\n};\n\nexport const Default: Story = {\n  render: () => <DemoDrawer />,\n};\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/drawer/drawer.tsx",
    "content": "import * as DrawerPrimitive from '@radix-ui/react-dialog';\nimport { Cross2Icon } from '@radix-ui/react-icons';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport * as React from 'react';\n\nimport { cn } from '@/utils/cn';\n\nconst Drawer = DrawerPrimitive.Root;\n\nconst DrawerTrigger = DrawerPrimitive.Trigger;\n\nconst DrawerClose = DrawerPrimitive.Close;\n\nconst DrawerPortal = DrawerPrimitive.Portal;\n\nconst DrawerOverlay = React.forwardRef<\n  React.ElementRef<typeof DrawerPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <DrawerPrimitive.Overlay\n    className={cn(\n      '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',\n      className,\n    )}\n    {...props}\n    ref={ref}\n  />\n));\nDrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;\n\nconst drawerVariants = cva(\n  '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',\n  {\n    variants: {\n      side: {\n        top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',\n        bottom:\n          'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',\n        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',\n        right:\n          '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',\n      },\n    },\n    defaultVariants: {\n      side: 'right',\n    },\n  },\n);\n\ntype DrawerContentProps = React.ComponentPropsWithoutRef<\n  typeof DrawerPrimitive.Content\n> &\n  VariantProps<typeof drawerVariants>;\n\nconst DrawerContent = React.forwardRef<\n  React.ElementRef<typeof DrawerPrimitive.Content>,\n  DrawerContentProps\n>(({ side = 'right', className, children, ...props }, ref) => (\n  <DrawerPortal>\n    <DrawerOverlay />\n    <DrawerPrimitive.Content\n      ref={ref}\n      className={cn(drawerVariants({ side }), className)}\n      {...props}\n    >\n      {children}\n      <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\">\n        <Cross2Icon className=\"size-4\" />\n        <span className=\"sr-only\">Close</span>\n      </DrawerPrimitive.Close>\n    </DrawerPrimitive.Content>\n  </DrawerPortal>\n));\nDrawerContent.displayName = DrawerPrimitive.Content.displayName;\n\nconst DrawerHeader = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      'flex flex-col space-y-2 text-center sm:text-left',\n      className,\n    )}\n    {...props}\n  />\n);\nDrawerHeader.displayName = 'DrawerHeader';\n\nconst DrawerFooter = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',\n      className,\n    )}\n    {...props}\n  />\n);\nDrawerFooter.displayName = 'DrawerFooter';\n\nconst DrawerTitle = React.forwardRef<\n  React.ElementRef<typeof DrawerPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <DrawerPrimitive.Title\n    ref={ref}\n    className={cn('text-lg font-semibold text-foreground', className)}\n    {...props}\n  />\n));\nDrawerTitle.displayName = DrawerPrimitive.Title.displayName;\n\nconst DrawerDescription = React.forwardRef<\n  React.ElementRef<typeof DrawerPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <DrawerPrimitive.Description\n    ref={ref}\n    className={cn('text-sm text-muted-foreground', className)}\n    {...props}\n  />\n));\nDrawerDescription.displayName = DrawerPrimitive.Description.displayName;\n\nexport {\n  Drawer,\n  DrawerPortal,\n  DrawerOverlay,\n  DrawerTrigger,\n  DrawerClose,\n  DrawerContent,\n  DrawerHeader,\n  DrawerFooter,\n  DrawerTitle,\n  DrawerDescription,\n};\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/drawer/index.ts",
    "content": "export * from './drawer';\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/dropdown/dropdown.stories.tsx",
    "content": "import type { Meta } from '@storybook/react';\nimport React from 'react';\n\nimport { Button } from '@/components/ui/button';\n\nimport {\n  DropdownMenu,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuCheckboxItem,\n  DropdownMenuRadioItem,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuSub,\n  DropdownMenuSubTrigger,\n  DropdownMenuSubContent,\n  DropdownMenuRadioGroup,\n} from './dropdown';\n\nconst meta: Meta = {\n  component: DropdownMenu,\n};\n\nexport default meta;\n\nexport const Default = () => (\n  <DropdownMenu>\n    <DropdownMenuTrigger asChild>\n      <Button>Open Menu</Button>\n    </DropdownMenuTrigger>\n    <DropdownMenuContent>\n      <DropdownMenuItem>Item One</DropdownMenuItem>\n      <DropdownMenuItem>Item Two</DropdownMenuItem>\n      <DropdownMenuSeparator />\n      <DropdownMenuItem>Item Three</DropdownMenuItem>\n    </DropdownMenuContent>\n  </DropdownMenu>\n);\n\nexport const WithCheckboxItems = () => {\n  const [checked, setChecked] = React.useState(true);\n  const [checked2, setChecked2] = React.useState(false);\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <Button>Open Menu</Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent>\n        <DropdownMenuCheckboxItem\n          checked={checked}\n          onCheckedChange={setChecked}\n        >\n          Option One\n        </DropdownMenuCheckboxItem>\n        <DropdownMenuCheckboxItem\n          checked={checked2}\n          onCheckedChange={setChecked2}\n        >\n          Option Two\n        </DropdownMenuCheckboxItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n};\n\nexport const WithRadioItems = () => {\n  const [value, setValue] = React.useState('one');\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <Button>Open Menu</Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent>\n        <DropdownMenuLabel>Select an option</DropdownMenuLabel>\n        <DropdownMenuSeparator />\n        <DropdownMenuRadioGroup value={value} onValueChange={setValue}>\n          <DropdownMenuRadioItem value=\"one\">Option One</DropdownMenuRadioItem>\n          <DropdownMenuRadioItem value=\"two\">Option Two</DropdownMenuRadioItem>\n          <DropdownMenuRadioItem value=\"three\">\n            Option Three\n          </DropdownMenuRadioItem>\n        </DropdownMenuRadioGroup>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n};\n\nexport const WithSubmenus = () => (\n  <DropdownMenu>\n    <DropdownMenuTrigger>\n      <Button>Open Menu</Button>\n    </DropdownMenuTrigger>\n    <DropdownMenuContent>\n      <DropdownMenuItem>Item One</DropdownMenuItem>\n      <DropdownMenuSub>\n        <DropdownMenuSubTrigger>More Options</DropdownMenuSubTrigger>\n        <DropdownMenuSubContent>\n          <DropdownMenuItem>Sub Item One</DropdownMenuItem>\n          <DropdownMenuItem>Sub Item Two</DropdownMenuItem>\n        </DropdownMenuSubContent>\n      </DropdownMenuSub>\n      <DropdownMenuItem>Item Three</DropdownMenuItem>\n    </DropdownMenuContent>\n  </DropdownMenu>\n);\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/dropdown/dropdown.tsx",
    "content": "import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';\nimport {\n  CheckIcon,\n  ChevronRightIcon,\n  DotFilledIcon,\n} from '@radix-ui/react-icons';\nimport * as React from 'react';\n\nimport { cn } from '@/utils/cn';\n\nconst DropdownMenu = DropdownMenuPrimitive.Root;\n\nconst DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;\n\nconst DropdownMenuGroup = DropdownMenuPrimitive.Group;\n\nconst DropdownMenuPortal = DropdownMenuPrimitive.Portal;\n\nconst DropdownMenuSub = DropdownMenuPrimitive.Sub;\n\nconst DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;\n\nconst DropdownMenuSubTrigger = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {\n    inset?: boolean;\n  }\n>(({ className, inset, children, ...props }, ref) => (\n  <DropdownMenuPrimitive.SubTrigger\n    ref={ref}\n    className={cn(\n      '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',\n      inset && 'pl-8',\n      className,\n    )}\n    {...props}\n  >\n    {children}\n    <ChevronRightIcon className=\"ml-auto size-4\" />\n  </DropdownMenuPrimitive.SubTrigger>\n));\nDropdownMenuSubTrigger.displayName =\n  DropdownMenuPrimitive.SubTrigger.displayName;\n\nconst DropdownMenuSubContent = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>\n>(({ className, ...props }, ref) => (\n  <DropdownMenuPrimitive.SubContent\n    ref={ref}\n    className={cn(\n      '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',\n      className,\n    )}\n    {...props}\n  />\n));\nDropdownMenuSubContent.displayName =\n  DropdownMenuPrimitive.SubContent.displayName;\n\nconst DropdownMenuContent = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>\n>(({ className, sideOffset = 4, ...props }, ref) => (\n  <DropdownMenuPrimitive.Portal>\n    <DropdownMenuPrimitive.Content\n      ref={ref}\n      sideOffset={sideOffset}\n      className={cn(\n        'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md',\n        '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',\n        className,\n      )}\n      {...props}\n    />\n  </DropdownMenuPrimitive.Portal>\n));\nDropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;\n\nconst DropdownMenuItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {\n    inset?: boolean;\n  }\n>(({ className, inset, ...props }, ref) => (\n  <DropdownMenuPrimitive.Item\n    ref={ref}\n    className={cn(\n      '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',\n      inset && 'pl-8',\n      className,\n    )}\n    {...props}\n  />\n));\nDropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;\n\nconst DropdownMenuCheckboxItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>\n>(({ className, children, checked, ...props }, ref) => (\n  <DropdownMenuPrimitive.CheckboxItem\n    ref={ref}\n    className={cn(\n      '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',\n      className,\n    )}\n    checked={checked}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex size-3.5 items-center justify-center\">\n      <DropdownMenuPrimitive.ItemIndicator>\n        <CheckIcon className=\"size-4\" />\n      </DropdownMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </DropdownMenuPrimitive.CheckboxItem>\n));\nDropdownMenuCheckboxItem.displayName =\n  DropdownMenuPrimitive.CheckboxItem.displayName;\n\nconst DropdownMenuRadioItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>\n>(({ className, children, ...props }, ref) => (\n  <DropdownMenuPrimitive.RadioItem\n    ref={ref}\n    className={cn(\n      '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',\n      className,\n    )}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex size-3.5 items-center justify-center\">\n      <DropdownMenuPrimitive.ItemIndicator>\n        <DotFilledIcon className=\"size-4 fill-current\" />\n      </DropdownMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </DropdownMenuPrimitive.RadioItem>\n));\nDropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;\n\nconst DropdownMenuLabel = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {\n    inset?: boolean;\n  }\n>(({ className, inset, ...props }, ref) => (\n  <DropdownMenuPrimitive.Label\n    ref={ref}\n    className={cn(\n      'px-2 py-1.5 text-sm font-semibold',\n      inset && 'pl-8',\n      className,\n    )}\n    {...props}\n  />\n));\nDropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;\n\nconst DropdownMenuSeparator = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <DropdownMenuPrimitive.Separator\n    ref={ref}\n    className={cn('-mx-1 my-1 h-px bg-muted', className)}\n    {...props}\n  />\n));\nDropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;\n\nconst DropdownMenuShortcut = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLSpanElement>) => {\n  return (\n    <span\n      className={cn('ml-auto text-xs tracking-widest opacity-60', className)}\n      {...props}\n    />\n  );\n};\nDropdownMenuShortcut.displayName = 'DropdownMenuShortcut';\n\nexport {\n  DropdownMenu,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuCheckboxItem,\n  DropdownMenuRadioItem,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuShortcut,\n  DropdownMenuGroup,\n  DropdownMenuPortal,\n  DropdownMenuSub,\n  DropdownMenuSubContent,\n  DropdownMenuSubTrigger,\n  DropdownMenuRadioGroup,\n};\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/dropdown/index.ts",
    "content": "export * from './dropdown';\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/form/__tests__/form.test.tsx",
    "content": "import { SubmitHandler } from 'react-hook-form';\nimport { z } from 'zod';\n\nimport { Button } from '@/components/ui/button';\nimport { rtlRender, screen, waitFor, userEvent } from '@/testing/test-utils';\n\nimport { Form } from '../form';\nimport { Input } from '../input';\n\nconst testData = {\n  title: 'Hello World',\n};\n\nconst schema = z.object({\n  title: z.string().min(1, 'Required'),\n});\n\ntest('should render and submit a basic Form component', async () => {\n  const handleSubmit = vi.fn() as SubmitHandler<z.infer<typeof schema>>;\n\n  rtlRender(\n    <Form onSubmit={handleSubmit} schema={schema} id=\"my-form\">\n      {({ register, formState }) => (\n        <>\n          <Input\n            label=\"Title\"\n            error={formState.errors['title']}\n            registration={register('title')}\n          />\n\n          <Button name=\"submit\" type=\"submit\" className=\"w-full\">\n            Submit\n          </Button>\n        </>\n      )}\n    </Form>,\n  );\n\n  await userEvent.type(screen.getByLabelText(/title/i), testData.title);\n\n  await userEvent.click(screen.getByRole('button', { name: /submit/i }));\n\n  await waitFor(() =>\n    expect(handleSubmit).toHaveBeenCalledWith(testData, expect.anything()),\n  );\n});\n\ntest('should fail submission if validation fails', async () => {\n  const handleSubmit = vi.fn() as SubmitHandler<z.infer<typeof schema>>;\n\n  rtlRender(\n    <Form onSubmit={handleSubmit} schema={schema} id=\"my-form\">\n      {({ register, formState }) => (\n        <>\n          <Input\n            label=\"Title\"\n            error={formState.errors['title']}\n            registration={register('title')}\n          />\n\n          <Button name=\"submit\" type=\"submit\" className=\"w-full\">\n            Submit\n          </Button>\n        </>\n      )}\n    </Form>,\n  );\n\n  await userEvent.click(screen.getByRole('button', { name: /submit/i }));\n\n  await screen.findByRole('alert', { name: /required/i });\n\n  expect(handleSubmit).toHaveBeenCalledTimes(0);\n});\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/form/error.tsx",
    "content": "export type ErrorProps = {\n  errorMessage?: string | null;\n};\n\nexport const Error = ({ errorMessage }: ErrorProps) => {\n  if (!errorMessage) return null;\n\n  return (\n    <div\n      role=\"alert\"\n      aria-label={errorMessage}\n      className=\"text-sm font-semibold text-red-500\"\n    >\n      {errorMessage}\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/form/field-wrapper.tsx",
    "content": "import * as React from 'react';\nimport { type FieldError } from 'react-hook-form';\n\nimport { Error } from './error';\nimport { Label } from './label';\n\ntype FieldWrapperProps = {\n  label?: string;\n  className?: string;\n  children: React.ReactNode;\n  error?: FieldError | undefined;\n};\n\nexport type FieldWrapperPassThroughProps = Omit<\n  FieldWrapperProps,\n  'className' | 'children'\n>;\n\nexport const FieldWrapper = (props: FieldWrapperProps) => {\n  const { label, error, children } = props;\n  return (\n    <div>\n      <Label>\n        {label}\n        <div className=\"mt-1\">{children}</div>\n      </Label>\n      <Error errorMessage={error?.message} />\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/form/form-drawer.tsx",
    "content": "import * as React from 'react';\n\nimport { useDisclosure } from '@/hooks/use-disclosure';\n\nimport { Button } from '../button';\nimport {\n  Drawer,\n  DrawerClose,\n  DrawerContent,\n  DrawerFooter,\n  DrawerHeader,\n  DrawerTrigger,\n  DrawerTitle,\n} from '../drawer';\n\ntype FormDrawerProps = {\n  isDone: boolean;\n  triggerButton: React.ReactElement;\n  submitButton: React.ReactElement;\n  title: string;\n  children: React.ReactNode;\n};\n\nexport const FormDrawer = ({\n  title,\n  children,\n  isDone,\n  triggerButton,\n  submitButton,\n}: FormDrawerProps) => {\n  const { close, open, isOpen } = useDisclosure();\n\n  React.useEffect(() => {\n    if (isDone) {\n      close();\n    }\n  }, [isDone, close]);\n\n  return (\n    <Drawer\n      open={isOpen}\n      onOpenChange={(isOpen) => {\n        if (!isOpen) {\n          close();\n        } else {\n          open();\n        }\n      }}\n    >\n      <DrawerTrigger asChild>{triggerButton}</DrawerTrigger>\n      <DrawerContent className=\"flex max-w-[800px] flex-col justify-between sm:max-w-[540px]\">\n        <div className=\"flex flex-col\">\n          <DrawerHeader>\n            <DrawerTitle>{title}</DrawerTitle>\n          </DrawerHeader>\n          <div>{children}</div>\n        </div>\n        <DrawerFooter>\n          <DrawerClose asChild>\n            <Button variant=\"outline\" type=\"submit\">\n              Close\n            </Button>\n          </DrawerClose>\n          {submitButton}\n        </DrawerFooter>\n      </DrawerContent>\n    </Drawer>\n  );\n};\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/form/form.stories.tsx",
    "content": "import { Meta, StoryObj } from '@storybook/react';\nimport { z } from 'zod';\n\nimport { Button } from '../button';\n\nimport { Form } from './form';\nimport { FormDrawer } from './form-drawer';\nimport { Input } from './input';\nimport { Select } from './select';\nimport { Textarea } from './textarea';\n\nconst MyForm = ({ hideSubmit = false }: { hideSubmit?: boolean }) => {\n  return (\n    <Form\n      onSubmit={async (values) => {\n        alert(JSON.stringify(values, null, 2));\n      }}\n      schema={z.object({\n        title: z.string().min(1, 'Required'),\n        description: z.string().min(1, 'Required'),\n        type: z.string().min(1, 'Required'),\n      })}\n      id=\"my-form\"\n    >\n      {({ register, formState }) => (\n        <>\n          <Input\n            label=\"Title\"\n            error={formState.errors['title']}\n            registration={register('title')}\n          />\n          <Textarea\n            label=\"Description\"\n            error={formState.errors['description']}\n            registration={register('description')}\n          />\n          <Select\n            label=\"Type\"\n            error={formState.errors['type']}\n            registration={register('type')}\n            options={['A', 'B', 'C'].map((type) => ({\n              label: type,\n              value: type,\n            }))}\n          />\n\n          {!hideSubmit && (\n            <div>\n              <Button type=\"submit\" className=\"w-full\">\n                Submit\n              </Button>\n            </div>\n          )}\n        </>\n      )}\n    </Form>\n  );\n};\n\nconst meta: Meta = {\n  component: MyForm,\n};\n\nexport default meta;\n\ntype Story = StoryObj<typeof MyForm>;\n\nexport const Default: Story = {\n  render: () => <MyForm />,\n};\n\nexport const AsFormDrawer: Story = {\n  render: () => (\n    <FormDrawer\n      triggerButton={<Button>Open Form</Button>}\n      isDone={true}\n      title=\"My Form\"\n      submitButton={\n        <Button form=\"my-form\" type=\"submit\">\n          Submit\n        </Button>\n      }\n    >\n      <MyForm hideSubmit />\n    </FormDrawer>\n  ),\n};\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/form/form.tsx",
    "content": "import { zodResolver } from '@hookform/resolvers/zod';\nimport * as LabelPrimitive from '@radix-ui/react-label';\nimport { Slot } from '@radix-ui/react-slot';\nimport * as React from 'react';\nimport {\n  Controller,\n  ControllerProps,\n  FieldPath,\n  FieldValues,\n  FormProvider,\n  SubmitHandler,\n  UseFormProps,\n  UseFormReturn,\n  useForm,\n  useFormContext,\n} from 'react-hook-form';\nimport { ZodType, z } from 'zod';\n\nimport { cn } from '@/utils/cn';\n\nimport { Label } from './label';\n\ntype FormFieldContextValue<\n  TFieldValues extends FieldValues = FieldValues,\n  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n> = {\n  name: TName;\n};\n\nconst FormFieldContext = React.createContext<FormFieldContextValue>(\n  {} as FormFieldContextValue,\n);\n\nconst FormField = <\n  TFieldValues extends FieldValues,\n  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n>({\n  ...props\n}: ControllerProps<TFieldValues, TName>) => {\n  return (\n    <FormFieldContext.Provider value={{ name: props.name }}>\n      <Controller {...props} />\n    </FormFieldContext.Provider>\n  );\n};\n\nconst useFormField = () => {\n  const fieldContext = React.useContext(FormFieldContext);\n  const itemContext = React.useContext(FormItemContext);\n  const { getFieldState, formState } = useFormContext();\n\n  const fieldState = getFieldState(fieldContext.name, formState);\n\n  if (!fieldContext) {\n    throw new Error('useFormField should be used within <FormField>');\n  }\n\n  const { id } = itemContext;\n\n  return {\n    id,\n    name: fieldContext.name,\n    formItemId: `${id}-form-item`,\n    formDescriptionId: `${id}-form-item-description`,\n    formMessageId: `${id}-form-item-message`,\n    ...fieldState,\n  };\n};\n\ntype FormItemContextValue = {\n  id: string;\n};\n\nconst FormItemContext = React.createContext<FormItemContextValue>(\n  {} as FormItemContextValue,\n);\n\nconst FormItem = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => {\n  const id = React.useId();\n\n  return (\n    <FormItemContext.Provider value={{ id }}>\n      <div ref={ref} className={cn('space-y-2', className)} {...props} />\n    </FormItemContext.Provider>\n  );\n});\nFormItem.displayName = 'FormItem';\n\nconst FormLabel = React.forwardRef<\n  React.ElementRef<typeof LabelPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>\n>(({ className, ...props }, ref) => {\n  const { error, formItemId } = useFormField();\n\n  return (\n    <Label\n      ref={ref}\n      className={cn(error && 'text-destructive', className)}\n      htmlFor={formItemId}\n      {...props}\n    />\n  );\n});\nFormLabel.displayName = 'FormLabel';\n\nconst FormControl = React.forwardRef<\n  React.ElementRef<typeof Slot>,\n  React.ComponentPropsWithoutRef<typeof Slot>\n>(({ ...props }, ref) => {\n  const { error, formItemId, formDescriptionId, formMessageId } =\n    useFormField();\n\n  return (\n    <Slot\n      ref={ref}\n      id={formItemId}\n      aria-describedby={\n        !error\n          ? `${formDescriptionId}`\n          : `${formDescriptionId} ${formMessageId}`\n      }\n      aria-invalid={!!error}\n      {...props}\n    />\n  );\n});\nFormControl.displayName = 'FormControl';\n\nconst FormDescription = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, ...props }, ref) => {\n  const { formDescriptionId } = useFormField();\n\n  return (\n    <p\n      ref={ref}\n      id={formDescriptionId}\n      className={cn('text-[0.8rem] text-muted-foreground', className)}\n      {...props}\n    />\n  );\n});\nFormDescription.displayName = 'FormDescription';\n\nconst FormMessage = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, children, ...props }, ref) => {\n  const { error, formMessageId } = useFormField();\n  const body = error ? String(error?.message) : children;\n\n  if (!body) {\n    return null;\n  }\n\n  return (\n    <p\n      ref={ref}\n      id={formMessageId}\n      className={cn('text-[0.8rem] font-medium text-destructive', className)}\n      {...props}\n    >\n      {body}\n    </p>\n  );\n});\nFormMessage.displayName = 'FormMessage';\n\ntype FormProps<TFormValues extends FieldValues, Schema> = {\n  onSubmit: SubmitHandler<TFormValues>;\n  schema: Schema;\n  className?: string;\n  children: (methods: UseFormReturn<TFormValues>) => React.ReactNode;\n  options?: UseFormProps<TFormValues>;\n  id?: string;\n};\n\nconst Form = <\n  Schema extends ZodType<any, any, any>,\n  TFormValues extends FieldValues = z.infer<Schema>,\n>({\n  onSubmit,\n  children,\n  className,\n  options,\n  id,\n  schema,\n}: FormProps<TFormValues, Schema>) => {\n  const form = useForm({ ...options, resolver: zodResolver(schema) });\n  return (\n    <FormProvider {...form}>\n      <form\n        className={cn('space-y-6', className)}\n        onSubmit={form.handleSubmit(onSubmit)}\n        id={id}\n      >\n        {children(form)}\n      </form>\n    </FormProvider>\n  );\n};\n\nexport {\n  useFormField,\n  Form,\n  FormProvider,\n  FormItem,\n  FormLabel,\n  FormControl,\n  FormDescription,\n  FormMessage,\n  FormField,\n};\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/form/index.ts",
    "content": "export * from './form';\nexport * from './input';\nexport * from './select';\nexport * from './textarea';\nexport * from './form-drawer';\nexport * from './label';\nexport * from './switch';\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/form/input.tsx",
    "content": "import * as React from 'react';\nimport { type UseFormRegisterReturn } from 'react-hook-form';\n\nimport { cn } from '@/utils/cn';\n\nimport { FieldWrapper, FieldWrapperPassThroughProps } from './field-wrapper';\n\nexport type InputProps = React.InputHTMLAttributes<HTMLInputElement> &\n  FieldWrapperPassThroughProps & {\n    className?: string;\n    registration: Partial<UseFormRegisterReturn>;\n  };\n\nconst Input = React.forwardRef<HTMLInputElement, InputProps>(\n  ({ className, type, label, error, registration, ...props }, ref) => {\n    return (\n      <FieldWrapper label={label} error={error}>\n        <input\n          type={type}\n          className={cn(\n            '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',\n            className,\n          )}\n          ref={ref}\n          {...registration}\n          {...props}\n        />\n      </FieldWrapper>\n    );\n  },\n);\nInput.displayName = 'Input';\n\nexport { Input };\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/form/label.tsx",
    "content": "import * as LabelPrimitive from '@radix-ui/react-label';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport * as React from 'react';\n\nimport { cn } from '@/utils/cn';\n\nconst labelVariants = cva(\n  'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',\n);\n\nconst Label = React.forwardRef<\n  React.ElementRef<typeof LabelPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &\n    VariantProps<typeof labelVariants>\n>(({ className, ...props }, ref) => (\n  <LabelPrimitive.Root\n    ref={ref}\n    className={cn(labelVariants(), className)}\n    {...props}\n  />\n));\nLabel.displayName = LabelPrimitive.Root.displayName;\n\nexport { Label };\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/form/select.tsx",
    "content": "import * as React from 'react';\nimport { UseFormRegisterReturn } from 'react-hook-form';\n\nimport { cn } from '@/utils/cn';\n\nimport { FieldWrapper, FieldWrapperPassThroughProps } from './field-wrapper';\n\ntype Option = {\n  label: React.ReactNode;\n  value: string | number | string[];\n};\n\ntype SelectFieldProps = FieldWrapperPassThroughProps & {\n  options: Option[];\n  className?: string;\n  defaultValue?: string;\n  registration: Partial<UseFormRegisterReturn>;\n};\n\nexport const Select = (props: SelectFieldProps) => {\n  const { label, options, error, className, defaultValue, registration } =\n    props;\n  return (\n    <FieldWrapper label={label} error={error}>\n      <select\n        className={cn(\n          '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',\n          className,\n        )}\n        defaultValue={defaultValue}\n        {...registration}\n      >\n        {options.map(({ label, value }) => (\n          <option key={label?.toString()} value={value}>\n            {label}\n          </option>\n        ))}\n      </select>\n    </FieldWrapper>\n  );\n};\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/form/switch.tsx",
    "content": "import * as SwitchPrimitives from '@radix-ui/react-switch';\nimport * as React from 'react';\n\nimport { cn } from '@/utils/cn';\n\nconst Switch = React.forwardRef<\n  React.ElementRef<typeof SwitchPrimitives.Root>,\n  React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>\n>(({ className, ...props }, ref) => (\n  <SwitchPrimitives.Root\n    className={cn(\n      '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',\n      className,\n    )}\n    {...props}\n    ref={ref}\n  >\n    <SwitchPrimitives.Thumb\n      className={cn(\n        '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',\n      )}\n    />\n  </SwitchPrimitives.Root>\n));\nSwitch.displayName = SwitchPrimitives.Root.displayName;\n\nexport { Switch };\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/form/textarea.tsx",
    "content": "import * as React from 'react';\nimport { UseFormRegisterReturn } from 'react-hook-form';\n\nimport { cn } from '@/utils/cn';\n\nimport { FieldWrapper, FieldWrapperPassThroughProps } from './field-wrapper';\n\nexport type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement> &\n  FieldWrapperPassThroughProps & {\n    className?: string;\n    registration: Partial<UseFormRegisterReturn>;\n  };\n\nconst Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(\n  ({ className, label, error, registration, ...props }, ref) => {\n    return (\n      <FieldWrapper label={label} error={error}>\n        <textarea\n          className={cn(\n            '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',\n            className,\n          )}\n          ref={ref}\n          {...registration}\n          {...props}\n        />\n      </FieldWrapper>\n    );\n  },\n);\nTextarea.displayName = 'Textarea';\n\nexport { Textarea };\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/link/index.ts",
    "content": "export * from './link';\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/link/link.stories.tsx",
    "content": "import { Meta, StoryObj } from '@storybook/react';\n\nimport { Link } from './link';\n\nconst meta: Meta<typeof Link> = {\n  component: Link,\n};\n\nexport default meta;\n\ntype Story = StoryObj<typeof Link>;\n\nexport const Default: Story = {\n  args: {\n    children: 'Link',\n    to: '/',\n  },\n};\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/link/link.tsx",
    "content": "import { Link as RouterLink, LinkProps } from 'react-router';\n\nimport { cn } from '@/utils/cn';\n\nexport const Link = ({ className, children, ...props }: LinkProps) => {\n  return (\n    <RouterLink\n      className={cn('text-slate-600 hover:text-slate-900', className)}\n      {...props}\n    >\n      {children}\n    </RouterLink>\n  );\n};\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/md-preview/index.ts",
    "content": "export * from './md-preview';\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/md-preview/md-preview.stories.tsx",
    "content": "import { Meta, StoryObj } from '@storybook/react';\n\nimport { MDPreview } from './md-preview';\n\nconst meta: Meta<typeof MDPreview> = {\n  component: MDPreview,\n};\n\nexport default meta;\n\ntype Story = StoryObj<typeof MDPreview>;\n\nexport const Default: Story = {\n  args: {\n    value: `## Hello World!`,\n  },\n};\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/md-preview/md-preview.tsx",
    "content": "import createDOMPurify from 'dompurify';\nimport { parse } from 'marked';\n\nconst DOMPurify = createDOMPurify(window);\n\nexport type MDPreviewProps = {\n  value: string;\n};\n\nexport const MDPreview = ({ value = '' }: MDPreviewProps) => {\n  return (\n    <div\n      className=\"prose prose-slate w-full p-2\"\n      dangerouslySetInnerHTML={{\n        __html: DOMPurify.sanitize(parse(value) as string),\n      }}\n    />\n  );\n};\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/notifications/__tests__/notifications.test.ts",
    "content": "import { renderHook, act } from '@testing-library/react';\n\nimport { useNotifications, Notification } from '../notifications-store';\n\ntest('should add and remove notifications', () => {\n  const { result } = renderHook(() => useNotifications());\n\n  expect(result.current.notifications.length).toBe(0);\n\n  const notification: Notification = {\n    id: '123',\n    title: 'Hello World',\n    type: 'info',\n    message: 'This is a notification',\n  };\n\n  act(() => {\n    result.current.addNotification(notification);\n  });\n\n  expect(result.current.notifications).toContainEqual(notification);\n\n  act(() => {\n    result.current.dismissNotification(notification.id);\n  });\n\n  expect(result.current.notifications).not.toContainEqual(notification);\n});\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/notifications/index.ts",
    "content": "export * from './notifications';\nexport * from './notifications-store';\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/notifications/notification.stories.tsx",
    "content": "import { Meta, StoryObj } from '@storybook/react';\n\nimport { Notification } from './notification';\n\nconst meta: Meta<typeof Notification> = {\n  title: 'Components/Notifications',\n  component: Notification,\n  parameters: {\n    controls: { expanded: true },\n  },\n};\n\nexport default meta;\n\ntype Story = StoryObj<typeof Notification>;\n\nexport const Info: Story = {\n  args: {\n    notification: {\n      id: '1',\n      type: 'info',\n      title: 'Hello Info',\n      message: 'This is info notification',\n    },\n    onDismiss: (id) => alert(`Dismissing Notification with id: ${id}`),\n  },\n};\n\nexport const Success: Story = {\n  args: {\n    notification: {\n      id: '1',\n      type: 'success',\n      title: 'Hello Success',\n      message: 'This is success notification',\n    },\n    onDismiss: (id) => alert(`Dismissing Notification with id: ${id}`),\n  },\n};\n\nexport const Warning: Story = {\n  args: {\n    notification: {\n      id: '1',\n      type: 'warning',\n      title: 'Hello Warning',\n      message: 'This is warning notification',\n    },\n    onDismiss: (id) => alert(`Dismissing Notification with id: ${id}`),\n  },\n};\n\nexport const Error: Story = {\n  args: {\n    notification: {\n      id: '1',\n      type: 'error',\n      title: 'Hello Error',\n      message: 'This is error notification',\n    },\n    onDismiss: (id) => alert(`Dismissing Notification with id: ${id}`),\n  },\n};\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/notifications/notification.tsx",
    "content": "import { Info, CircleAlert, CircleX, CircleCheck } from 'lucide-react';\n\nconst icons = {\n  info: <Info className=\"size-6 text-blue-500\" aria-hidden=\"true\" />,\n  success: <CircleCheck className=\"size-6 text-green-500\" aria-hidden=\"true\" />,\n  warning: (\n    <CircleAlert className=\"size-6 text-yellow-500\" aria-hidden=\"true\" />\n  ),\n  error: <CircleX className=\"size-6 text-red-500\" aria-hidden=\"true\" />,\n};\n\nexport type NotificationProps = {\n  notification: {\n    id: string;\n    type: keyof typeof icons;\n    title: string;\n    message?: string;\n  };\n  onDismiss: (id: string) => void;\n};\n\nexport const Notification = ({\n  notification: { id, type, title, message },\n  onDismiss,\n}: NotificationProps) => {\n  return (\n    <div className=\"flex w-full flex-col items-center space-y-4 sm:items-end\">\n      <div className=\"pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg bg-white shadow-lg ring-1 ring-black/5\">\n        <div className=\"p-4\" role=\"alert\" aria-label={title}>\n          <div className=\"flex items-start\">\n            <div className=\"shrink-0\">{icons[type]}</div>\n            <div className=\"ml-3 w-0 flex-1 pt-0.5\">\n              <p className=\"text-sm font-medium text-gray-900\">{title}</p>\n              <p className=\"mt-1 text-sm text-gray-500\">{message}</p>\n            </div>\n            <div className=\"ml-4 flex shrink-0\">\n              <button\n                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\"\n                onClick={() => {\n                  onDismiss(id);\n                }}\n              >\n                <span className=\"sr-only\">Close</span>\n                <CircleX className=\"size-5\" aria-hidden=\"true\" />\n              </button>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/notifications/notifications-store.ts",
    "content": "import { nanoid } from 'nanoid';\nimport { create } from 'zustand';\n\nexport type Notification = {\n  id: string;\n  type: 'info' | 'warning' | 'success' | 'error';\n  title: string;\n  message?: string;\n};\n\ntype NotificationsStore = {\n  notifications: Notification[];\n  addNotification: (notification: Omit<Notification, 'id'>) => void;\n  dismissNotification: (id: string) => void;\n};\n\nexport const useNotifications = create<NotificationsStore>((set) => ({\n  notifications: [],\n  addNotification: (notification) =>\n    set((state) => ({\n      notifications: [\n        ...state.notifications,\n        { id: nanoid(), ...notification },\n      ],\n    })),\n  dismissNotification: (id) =>\n    set((state) => ({\n      notifications: state.notifications.filter(\n        (notification) => notification.id !== id,\n      ),\n    })),\n}));\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/notifications/notifications.tsx",
    "content": "import { Notification } from './notification';\nimport { useNotifications } from './notifications-store';\n\nexport const Notifications = () => {\n  const { notifications, dismissNotification } = useNotifications();\n\n  return (\n    <div\n      aria-live=\"assertive\"\n      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\"\n    >\n      {notifications.map((notification) => (\n        <Notification\n          key={notification.id}\n          notification={notification}\n          onDismiss={dismissNotification}\n        />\n      ))}\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/spinner/index.ts",
    "content": "export * from './spinner';\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/spinner/spinner.stories.tsx",
    "content": "import { Meta, StoryObj } from '@storybook/react';\n\nimport { Spinner } from './spinner';\n\nconst meta: Meta<typeof Spinner> = {\n  component: Spinner,\n};\n\nexport default meta;\n\ntype Story = StoryObj<typeof Spinner>;\n\nexport const Default: Story = {\n  args: {\n    size: 'md',\n  },\n};\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/spinner/spinner.tsx",
    "content": "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',\n};\n\nconst variants = {\n  light: 'text-white',\n  primary: 'text-slate-600',\n};\n\nexport type SpinnerProps = {\n  size?: keyof typeof sizes;\n  variant?: keyof typeof variants;\n  className?: string;\n};\n\nexport const Spinner = ({\n  size = 'md',\n  variant = 'primary',\n  className = '',\n}: SpinnerProps) => {\n  return (\n    <>\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        width=\"24\"\n        height=\"24\"\n        viewBox=\"0 0 24 24\"\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeWidth=\"2\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        className={cn(\n          'animate-spin',\n          sizes[size],\n          variants[variant],\n          className,\n        )}\n      >\n        <path d=\"M21 12a9 9 0 1 1-6.219-8.56\" />\n      </svg>\n      <span className=\"sr-only\">Loading</span>\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/table/index.ts",
    "content": "export * from './table';\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/table/pagination.tsx",
    "content": "import {\n  ChevronLeftIcon,\n  ChevronRightIcon,\n  DotsHorizontalIcon,\n} from '@radix-ui/react-icons';\nimport * as React from 'react';\n\nimport { ButtonProps, buttonVariants } from '@/components/ui/button';\nimport { cn } from '@/utils/cn';\n\nimport { Link } from '../link';\n\nconst Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => (\n  <nav\n    role=\"navigation\"\n    aria-label=\"pagination\"\n    className={cn('mx-auto flex w-full justify-center', className)}\n    {...props}\n  />\n);\nPagination.displayName = 'Pagination';\n\nconst PaginationContent = React.forwardRef<\n  HTMLUListElement,\n  React.ComponentProps<'ul'>\n>(({ className, ...props }, ref) => (\n  <ul\n    ref={ref}\n    className={cn('flex flex-row items-center gap-1', className)}\n    {...props}\n  />\n));\nPaginationContent.displayName = 'PaginationContent';\n\nconst PaginationItem = React.forwardRef<\n  HTMLLIElement,\n  React.ComponentProps<'li'>\n>(({ className, ...props }, ref) => (\n  <li ref={ref} className={cn('', className)} {...props} />\n));\nPaginationItem.displayName = 'PaginationItem';\n\ntype PaginationLinkProps = {\n  isActive?: boolean;\n} & Pick<ButtonProps, 'size'> &\n  React.ComponentProps<'a'>;\n\nconst PaginationLink = ({\n  className,\n  isActive,\n  size = 'icon',\n  children,\n  href,\n  ...props\n}: PaginationLinkProps) => (\n  <Link\n    to={href as string}\n    aria-current={isActive ? 'page' : undefined}\n    className={cn(\n      buttonVariants({\n        variant: isActive ? 'outline' : 'ghost',\n        size,\n      }),\n      className,\n    )}\n    {...props}\n  >\n    {children}\n  </Link>\n);\nPaginationLink.displayName = 'PaginationLink';\n\nconst PaginationPrevious = ({\n  className,\n  ...props\n}: React.ComponentProps<typeof PaginationLink>) => (\n  <PaginationLink\n    aria-label=\"Go to previous page\"\n    size=\"default\"\n    className={cn('gap-1 pl-2.5', className)}\n    {...props}\n  >\n    <ChevronLeftIcon className=\"size-4\" />\n    <span>Previous</span>\n  </PaginationLink>\n);\nPaginationPrevious.displayName = 'PaginationPrevious';\n\nconst PaginationNext = ({\n  className,\n  ...props\n}: React.ComponentProps<typeof PaginationLink>) => (\n  <PaginationLink\n    aria-label=\"Go to next page\"\n    size=\"default\"\n    className={cn('gap-1 pr-2.5', className)}\n    {...props}\n  >\n    <span>Next</span>\n    <ChevronRightIcon className=\"size-4\" />\n  </PaginationLink>\n);\nPaginationNext.displayName = 'PaginationNext';\n\nconst PaginationEllipsis = ({\n  className,\n  ...props\n}: React.ComponentProps<'span'>) => (\n  <span\n    aria-hidden\n    className={cn('flex h-9 w-9 items-center justify-center', className)}\n    {...props}\n  >\n    <DotsHorizontalIcon className=\"size-4\" />\n    <span className=\"sr-only\">More pages</span>\n  </span>\n);\nPaginationEllipsis.displayName = 'PaginationEllipsis';\n\nexport {\n  Pagination,\n  PaginationContent,\n  PaginationLink,\n  PaginationItem,\n  PaginationPrevious,\n  PaginationNext,\n  PaginationEllipsis,\n};\n\nexport type TablePaginationProps = {\n  totalPages: number;\n  currentPage: number;\n  rootUrl: string;\n};\n\nexport const TablePagination = ({\n  totalPages,\n  currentPage,\n  rootUrl,\n}: TablePaginationProps) => {\n  const createHref = (page: number) => `${rootUrl}?page=${page}`;\n\n  return (\n    <Pagination className=\"justify-end py-8\">\n      <PaginationContent>\n        {currentPage > 1 && (\n          <PaginationItem>\n            <PaginationPrevious href={createHref(currentPage - 1)} />\n          </PaginationItem>\n        )}\n        {currentPage > 2 && (\n          <PaginationItem>\n            <PaginationEllipsis />\n          </PaginationItem>\n        )}\n        {currentPage > 1 && (\n          <PaginationItem>\n            <PaginationLink href={createHref(currentPage - 1)}>\n              {currentPage - 1}\n            </PaginationLink>\n          </PaginationItem>\n        )}\n        <PaginationItem className=\"rounded-sm bg-gray-200\">\n          <PaginationLink href={createHref(currentPage)}>\n            {currentPage}\n          </PaginationLink>\n        </PaginationItem>\n        {totalPages > currentPage && (\n          <PaginationItem>\n            <PaginationLink href={createHref(currentPage + 1)}>\n              {currentPage + 1}\n            </PaginationLink>\n          </PaginationItem>\n        )}\n        {totalPages > currentPage + 1 && (\n          <PaginationItem>\n            <PaginationEllipsis />\n          </PaginationItem>\n        )}\n        {currentPage < totalPages && (\n          <PaginationItem>\n            <PaginationNext href={createHref(totalPages)} />\n          </PaginationItem>\n        )}\n      </PaginationContent>\n    </Pagination>\n  );\n};\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/table/table.stories.tsx",
    "content": "import { Meta, StoryObj } from '@storybook/react';\n\nimport { Table } from './table';\n\nconst meta: Meta<typeof Table> = {\n  component: Table,\n};\n\nexport default meta;\n\ntype User = {\n  id: string;\n  createdAt: number;\n  name: string;\n  title: string;\n  role: string;\n  email: string;\n};\n\ntype Story = StoryObj<typeof Table<User>>;\n\nconst data: User[] = [\n  {\n    id: '1',\n    createdAt: Date.now(),\n    name: 'Jane Cooper',\n    title: 'Regional Paradigm Technician',\n    role: 'Admin',\n    email: 'jane.cooper@example.com',\n  },\n  {\n    id: '2',\n    createdAt: Date.now(),\n    name: 'Cody Fisher',\n    title: 'Product Directives Officer',\n    role: 'Owner',\n    email: 'cody.fisher@example.com',\n  },\n];\n\nexport const Default: Story = {\n  args: {\n    data,\n    columns: [\n      {\n        title: 'Name',\n        field: 'name',\n      },\n      {\n        title: 'Title',\n        field: 'title',\n      },\n      {\n        title: 'Role',\n        field: 'role',\n      },\n      {\n        title: 'Email',\n        field: 'email',\n      },\n    ],\n  },\n};\n"
  },
  {
    "path": "apps/react-vite/src/components/ui/table/table.tsx",
    "content": "import { ArchiveX } from 'lucide-react';\nimport * as React from 'react';\n\nimport { BaseEntity } from '@/types/api';\nimport { cn } from '@/utils/cn';\n\nimport { TablePagination, TablePaginationProps } from './pagination';\n\nconst TableElement = React.forwardRef<\n  HTMLTableElement,\n  React.HTMLAttributes<HTMLTableElement>\n>(({ className, ...props }, ref) => (\n  <div className=\"relative w-full overflow-auto\">\n    <table\n      ref={ref}\n      className={cn('w-full caption-bottom text-sm', className)}\n      {...props}\n    />\n  </div>\n));\nTableElement.displayName = 'Table';\n\nconst TableHeader = React.forwardRef<\n  HTMLTableSectionElement,\n  React.HTMLAttributes<HTMLTableSectionElement>\n>(({ className, ...props }, ref) => (\n  <thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />\n));\nTableHeader.displayName = 'TableHeader';\n\nconst TableBody = React.forwardRef<\n  HTMLTableSectionElement,\n  React.HTMLAttributes<HTMLTableSectionElement>\n>(({ className, ...props }, ref) => (\n  <tbody\n    ref={ref}\n    className={cn('[&_tr:last-child]:border-0', className)}\n    {...props}\n  />\n));\nTableBody.displayName = 'TableBody';\n\nconst TableFooter = React.forwardRef<\n  HTMLTableSectionElement,\n  React.HTMLAttributes<HTMLTableSectionElement>\n>(({ className, ...props }, ref) => (\n  <tfoot\n    ref={ref}\n    className={cn(\n      'border-t bg-muted/50 font-medium [&>tr]:last:border-b-0',\n      className,\n    )}\n    {...props}\n  />\n));\nTableFooter.displayName = 'TableFooter';\n\nconst TableRow = React.forwardRef<\n  HTMLTableRowElement,\n  React.HTMLAttributes<HTMLTableRowElement>\n>(({ className, ...props }, ref) => (\n  <tr\n    ref={ref}\n    className={cn(\n      'border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted',\n      className,\n    )}\n    {...props}\n  />\n));\nTableRow.displayName = 'TableRow';\n\nconst TableHead = React.forwardRef<\n  HTMLTableCellElement,\n  React.ThHTMLAttributes<HTMLTableCellElement>\n>(({ className, ...props }, ref) => (\n  <th\n    ref={ref}\n    className={cn(\n      'h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',\n      className,\n    )}\n    {...props}\n  />\n));\nTableHead.displayName = 'TableHead';\n\nconst TableCell = React.forwardRef<\n  HTMLTableCellElement,\n  React.TdHTMLAttributes<HTMLTableCellElement>\n>(({ className, ...props }, ref) => (\n  <td\n    ref={ref}\n    className={cn(\n      'p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',\n      className,\n    )}\n    {...props}\n  />\n));\nTableCell.displayName = 'TableCell';\n\nconst TableCaption = React.forwardRef<\n  HTMLTableCaptionElement,\n  React.HTMLAttributes<HTMLTableCaptionElement>\n>(({ className, ...props }, ref) => (\n  <caption\n    ref={ref}\n    className={cn('mt-4 text-sm text-muted-foreground', className)}\n    {...props}\n  />\n));\nTableCaption.displayName = 'TableCaption';\n\nexport {\n  TableElement,\n  TableHeader,\n  TableBody,\n  TableFooter,\n  TableHead,\n  TableRow,\n  TableCell,\n  TableCaption,\n};\n\ntype TableColumn<Entry> = {\n  title: string;\n  field: keyof Entry;\n  Cell?({ entry }: { entry: Entry }): React.ReactElement;\n};\n\nexport type TableProps<Entry> = {\n  data: Entry[];\n  columns: TableColumn<Entry>[];\n  pagination?: TablePaginationProps;\n};\n\nexport const Table = <Entry extends BaseEntity>({\n  data,\n  columns,\n  pagination,\n}: TableProps<Entry>) => {\n  if (!data?.length) {\n    return (\n      <div className=\"flex h-80 flex-col items-center justify-center bg-white text-gray-500\">\n        <ArchiveX className=\"size-16\" />\n        <h4>No Entries Found</h4>\n      </div>\n    );\n  }\n  return (\n    <>\n      <TableElement>\n        <TableHeader>\n          <TableRow>\n            {columns.map((column, index) => (\n              <TableHead key={column.title + index}>{column.title}</TableHead>\n            ))}\n          </TableRow>\n        </TableHeader>\n        <TableBody>\n          {data.map((entry, entryIndex) => (\n            <TableRow key={entry?.id || entryIndex}>\n              {columns.map(({ Cell, field, title }, columnIndex) => (\n                <TableCell key={title + columnIndex}>\n                  {Cell ? <Cell entry={entry} /> : `${entry[field]}`}\n                </TableCell>\n              ))}\n            </TableRow>\n          ))}\n        </TableBody>\n      </TableElement>\n\n      {pagination && <TablePagination {...pagination} />}\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/react-vite/src/config/env.ts",
    "content": "import * as z from 'zod';\n\nconst createEnv = () => {\n  const EnvSchema = z.object({\n    API_URL: z.string(),\n    ENABLE_API_MOCKING: z\n      .string()\n      .refine((s) => s === 'true' || s === 'false')\n      .transform((s) => s === 'true')\n      .optional(),\n    APP_URL: z.string().optional().default('http://localhost:3000'),\n    APP_MOCK_API_PORT: z.string().optional().default('8080'),\n  });\n\n  const envVars = Object.entries(import.meta.env).reduce<\n    Record<string, string>\n  >((acc, curr) => {\n    const [key, value] = curr;\n    if (key.startsWith('VITE_APP_')) {\n      acc[key.replace('VITE_APP_', '')] = value;\n    }\n    return acc;\n  }, {});\n\n  const parsedEnv = EnvSchema.safeParse(envVars);\n\n  if (!parsedEnv.success) {\n    throw new Error(\n      `Invalid env provided.\nThe following variables are missing or invalid:\n${Object.entries(parsedEnv.error.flatten().fieldErrors)\n  .map(([k, v]) => `- ${k}: ${v}`)\n  .join('\\n')}\n`,\n    );\n  }\n\n  return parsedEnv.data;\n};\n\nexport const env = createEnv();\n"
  },
  {
    "path": "apps/react-vite/src/config/paths.ts",
    "content": "export const paths = {\n  home: {\n    path: '/',\n    getHref: () => '/',\n  },\n\n  auth: {\n    register: {\n      path: '/auth/register',\n      getHref: (redirectTo?: string | null | undefined) =>\n        `/auth/register${redirectTo ? `?redirectTo=${encodeURIComponent(redirectTo)}` : ''}`,\n    },\n    login: {\n      path: '/auth/login',\n      getHref: (redirectTo?: string | null | undefined) =>\n        `/auth/login${redirectTo ? `?redirectTo=${encodeURIComponent(redirectTo)}` : ''}`,\n    },\n  },\n\n  app: {\n    root: {\n      path: '/app',\n      getHref: () => '/app',\n    },\n    dashboard: {\n      path: '',\n      getHref: () => '/app',\n    },\n    discussions: {\n      path: 'discussions',\n      getHref: () => '/app/discussions',\n    },\n    discussion: {\n      path: 'discussions/:discussionId',\n      getHref: (id: string) => `/app/discussions/${id}`,\n    },\n    users: {\n      path: 'users',\n      getHref: () => '/app/users',\n    },\n    profile: {\n      path: 'profile',\n      getHref: () => '/app/profile',\n    },\n  },\n} as const;\n"
  },
  {
    "path": "apps/react-vite/src/features/auth/components/__tests__/login-form.test.tsx",
    "content": "import {\n  createUser,\n  renderApp,\n  screen,\n  userEvent,\n  waitFor,\n} from '@/testing/test-utils';\n\nimport { LoginForm } from '../login-form';\n\ntest('should login new user and call onSuccess cb which should navigate the user to the app', async () => {\n  const newUser = await createUser({ teamId: undefined });\n\n  const onSuccess = vi.fn();\n\n  await renderApp(<LoginForm onSuccess={onSuccess} />, { user: null });\n\n  await userEvent.type(screen.getByLabelText(/email address/i), newUser.email);\n  await userEvent.type(screen.getByLabelText(/password/i), newUser.password);\n\n  await userEvent.click(screen.getByRole('button', { name: /log in/i }));\n\n  await waitFor(() => expect(onSuccess).toHaveBeenCalledTimes(1));\n});\n"
  },
  {
    "path": "apps/react-vite/src/features/auth/components/__tests__/register-form.test.tsx",
    "content": "import { createUser } from '@/testing/data-generators';\nimport { renderApp, screen, userEvent, waitFor } from '@/testing/test-utils';\n\nimport { RegisterForm } from '../register-form';\n\ntest('should register new user and call onSuccess cb which should navigate the user to the app', async () => {\n  const newUser = createUser({});\n\n  const onSuccess = vi.fn();\n\n  await renderApp(\n    <RegisterForm\n      onSuccess={onSuccess}\n      chooseTeam={false}\n      setChooseTeam={() => {}}\n      teams={[]}\n    />,\n    { user: null },\n  );\n\n  await userEvent.type(screen.getByLabelText(/first name/i), newUser.firstName);\n  await userEvent.type(screen.getByLabelText(/last name/i), newUser.lastName);\n  await userEvent.type(screen.getByLabelText(/email address/i), newUser.email);\n  await userEvent.type(screen.getByLabelText(/password/i), newUser.password);\n  await userEvent.type(screen.getByLabelText(/team name/i), newUser.teamName);\n\n  await userEvent.click(screen.getByRole('button', { name: /register/i }));\n\n  await waitFor(() => expect(onSuccess).toHaveBeenCalledTimes(1));\n});\n"
  },
  {
    "path": "apps/react-vite/src/features/auth/components/login-form.tsx",
    "content": "import { Link, useSearchParams } from 'react-router';\n\nimport { Button } from '@/components/ui/button';\nimport { Form, Input } from '@/components/ui/form';\nimport { paths } from '@/config/paths';\nimport { useLogin, loginInputSchema } from '@/lib/auth';\n\ntype LoginFormProps = {\n  onSuccess: () => void;\n};\n\nexport const LoginForm = ({ onSuccess }: LoginFormProps) => {\n  const login = useLogin({\n    onSuccess,\n  });\n  const [searchParams] = useSearchParams();\n  const redirectTo = searchParams.get('redirectTo');\n\n  return (\n    <div>\n      <Form\n        onSubmit={(values) => {\n          login.mutate(values);\n        }}\n        schema={loginInputSchema}\n      >\n        {({ register, formState }) => (\n          <>\n            <Input\n              type=\"email\"\n              label=\"Email Address\"\n              error={formState.errors['email']}\n              registration={register('email')}\n            />\n            <Input\n              type=\"password\"\n              label=\"Password\"\n              error={formState.errors['password']}\n              registration={register('password')}\n            />\n            <div>\n              <Button\n                isLoading={login.isPending}\n                type=\"submit\"\n                className=\"w-full\"\n              >\n                Log in\n              </Button>\n            </div>\n          </>\n        )}\n      </Form>\n      <div className=\"mt-2 flex items-center justify-end\">\n        <div className=\"text-sm\">\n          <Link\n            to={paths.auth.register.getHref(redirectTo)}\n            className=\"font-medium text-blue-600 hover:text-blue-500\"\n          >\n            Register\n          </Link>\n        </div>\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/react-vite/src/features/auth/components/register-form.tsx",
    "content": "import * as React from 'react';\nimport { Link, useSearchParams } from 'react-router';\n\nimport { Button } from '@/components/ui/button';\nimport { Form, Input, Select, Label, Switch } from '@/components/ui/form';\nimport { paths } from '@/config/paths';\nimport { useRegister, registerInputSchema } from '@/lib/auth';\nimport { Team } from '@/types/api';\n\ntype RegisterFormProps = {\n  onSuccess: () => void;\n  chooseTeam: boolean;\n  setChooseTeam: () => void;\n  teams?: Team[];\n};\n\nexport const RegisterForm = ({\n  onSuccess,\n  chooseTeam,\n  setChooseTeam,\n  teams,\n}: RegisterFormProps) => {\n  const registering = useRegister({ onSuccess });\n  const [searchParams] = useSearchParams();\n  const redirectTo = searchParams.get('redirectTo');\n\n  return (\n    <div>\n      <Form\n        onSubmit={(values) => {\n          registering.mutate(values);\n        }}\n        schema={registerInputSchema}\n        options={{\n          shouldUnregister: true,\n        }}\n      >\n        {({ register, formState }) => (\n          <>\n            <Input\n              type=\"text\"\n              label=\"First Name\"\n              error={formState.errors['firstName']}\n              registration={register('firstName')}\n            />\n            <Input\n              type=\"text\"\n              label=\"Last Name\"\n              error={formState.errors['lastName']}\n              registration={register('lastName')}\n            />\n            <Input\n              type=\"email\"\n              label=\"Email Address\"\n              error={formState.errors['email']}\n              registration={register('email')}\n            />\n            <Input\n              type=\"password\"\n              label=\"Password\"\n              error={formState.errors['password']}\n              registration={register('password')}\n            />\n\n            <div className=\"flex items-center space-x-2\">\n              <Switch\n                checked={chooseTeam}\n                onCheckedChange={setChooseTeam}\n                className={`${\n                  chooseTeam ? 'bg-blue-600' : 'bg-gray-200'\n                } 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`}\n                id=\"choose-team\"\n              />\n              <Label htmlFor=\"airplane-mode\">Join Existing Team</Label>\n            </div>\n\n            {chooseTeam && teams ? (\n              <Select\n                label=\"Team\"\n                error={formState.errors['teamId']}\n                registration={register('teamId')}\n                options={teams?.map((team) => ({\n                  label: team.name,\n                  value: team.id,\n                }))}\n              />\n            ) : (\n              <Input\n                type=\"text\"\n                label=\"Team Name\"\n                error={formState.errors['teamName']}\n                registration={register('teamName')}\n              />\n            )}\n            <div>\n              <Button\n                isLoading={registering.isPending}\n                type=\"submit\"\n                className=\"w-full\"\n              >\n                Register\n              </Button>\n            </div>\n          </>\n        )}\n      </Form>\n      <div className=\"mt-2 flex items-center justify-end\">\n        <div className=\"text-sm\">\n          <Link\n            to={paths.auth.login.getHref(redirectTo)}\n            className=\"font-medium text-blue-600 hover:text-blue-500\"\n          >\n            Log In\n          </Link>\n        </div>\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/react-vite/src/features/comments/api/create-comment.ts",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query';\nimport { z } from 'zod';\n\nimport { api } from '@/lib/api-client';\nimport { MutationConfig } from '@/lib/react-query';\nimport { Comment } from '@/types/api';\n\nimport { getInfiniteCommentsQueryOptions } from './get-comments';\n\nexport const createCommentInputSchema = z.object({\n  discussionId: z.string().min(1, 'Required'),\n  body: z.string().min(1, 'Required'),\n});\n\nexport type CreateCommentInput = z.infer<typeof createCommentInputSchema>;\n\nexport const createComment = ({\n  data,\n}: {\n  data: CreateCommentInput;\n}): Promise<Comment> => {\n  return api.post('/comments', data);\n};\n\ntype UseCreateCommentOptions = {\n  discussionId: string;\n  mutationConfig?: MutationConfig<typeof createComment>;\n};\n\nexport const useCreateComment = ({\n  mutationConfig,\n  discussionId,\n}: UseCreateCommentOptions) => {\n  const queryClient = useQueryClient();\n\n  const { onSuccess, ...restConfig } = mutationConfig || {};\n\n  return useMutation({\n    onSuccess: (...args) => {\n      queryClient.invalidateQueries({\n        queryKey: getInfiniteCommentsQueryOptions(discussionId).queryKey,\n      });\n      onSuccess?.(...args);\n    },\n    ...restConfig,\n    mutationFn: createComment,\n  });\n};\n"
  },
  {
    "path": "apps/react-vite/src/features/comments/api/delete-comment.ts",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query';\n\nimport { api } from '@/lib/api-client';\nimport { MutationConfig } from '@/lib/react-query';\n\nimport { getInfiniteCommentsQueryOptions } from './get-comments';\n\nexport const deleteComment = ({ commentId }: { commentId: string }) => {\n  return api.delete(`/comments/${commentId}`);\n};\n\ntype UseDeleteCommentOptions = {\n  discussionId: string;\n  mutationConfig?: MutationConfig<typeof deleteComment>;\n};\n\nexport const useDeleteComment = ({\n  mutationConfig,\n  discussionId,\n}: UseDeleteCommentOptions) => {\n  const queryClient = useQueryClient();\n\n  const { onSuccess, ...restConfig } = mutationConfig || {};\n\n  return useMutation({\n    onSuccess: (...args) => {\n      queryClient.invalidateQueries({\n        queryKey: getInfiniteCommentsQueryOptions(discussionId).queryKey,\n      });\n      onSuccess?.(...args);\n    },\n    ...restConfig,\n    mutationFn: deleteComment,\n  });\n};\n"
  },
  {
    "path": "apps/react-vite/src/features/comments/api/get-comments.ts",
    "content": "import { infiniteQueryOptions, useInfiniteQuery } from '@tanstack/react-query';\n\nimport { api } from '@/lib/api-client';\nimport { QueryConfig } from '@/lib/react-query';\nimport { Comment, Meta } from '@/types/api';\n\nexport const getComments = ({\n  discussionId,\n  page = 1,\n}: {\n  discussionId: string;\n  page?: number;\n}): Promise<{ data: Comment[]; meta: Meta }> => {\n  return api.get(`/comments`, {\n    params: {\n      discussionId,\n      page,\n    },\n  });\n};\n\nexport const getInfiniteCommentsQueryOptions = (discussionId: string) => {\n  return infiniteQueryOptions({\n    queryKey: ['comments', discussionId],\n    queryFn: ({ pageParam = 1 }) => {\n      return getComments({ discussionId, page: pageParam as number });\n    },\n    getNextPageParam: (lastPage) => {\n      if (lastPage?.meta?.page === lastPage?.meta?.totalPages) return undefined;\n      const nextPage = lastPage.meta.page + 1;\n      return nextPage;\n    },\n    initialPageParam: 1,\n  });\n};\n\ntype UseCommentsOptions = {\n  discussionId: string;\n  page?: number;\n  queryConfig?: QueryConfig<typeof getComments>;\n};\n\nexport const useInfiniteComments = ({ discussionId }: UseCommentsOptions) => {\n  return useInfiniteQuery({\n    ...getInfiniteCommentsQueryOptions(discussionId),\n  });\n};\n"
  },
  {
    "path": "apps/react-vite/src/features/comments/components/comments-list.tsx",
    "content": "import { ArchiveX } from 'lucide-react';\n\nimport { Button } from '@/components/ui/button';\nimport { MDPreview } from '@/components/ui/md-preview';\nimport { Spinner } from '@/components/ui/spinner';\nimport { useUser } from '@/lib/auth';\nimport { POLICIES, Authorization } from '@/lib/authorization';\nimport { User } from '@/types/api';\nimport { formatDate } from '@/utils/format';\n\nimport { useInfiniteComments } from '../api/get-comments';\n\nimport { DeleteComment } from './delete-comment';\n\ntype CommentsListProps = {\n  discussionId: string;\n};\n\nexport const CommentsList = ({ discussionId }: CommentsListProps) => {\n  const user = useUser();\n  const commentsQuery = useInfiniteComments({ discussionId });\n\n  if (commentsQuery.isLoading) {\n    return (\n      <div className=\"flex h-48 w-full items-center justify-center\">\n        <Spinner size=\"lg\" />\n      </div>\n    );\n  }\n\n  const comments = commentsQuery.data?.pages.flatMap((page) => page.data);\n\n  if (!comments?.length)\n    return (\n      <div\n        role=\"list\"\n        aria-label=\"comments\"\n        className=\"flex h-40 flex-col items-center justify-center bg-white text-gray-500\"\n      >\n        <ArchiveX className=\"size-10\" />\n        <h4>No Comments Found</h4>\n      </div>\n    );\n\n  return (\n    <>\n      <ul aria-label=\"comments\" className=\"flex flex-col space-y-3\">\n        {comments.map((comment, index) => (\n          <li\n            aria-label={`comment-${comment.body}-${index}`}\n            key={comment.id || index}\n            className=\"w-full bg-white p-4 shadow-sm\"\n          >\n            <Authorization\n              policyCheck={POLICIES['comment:delete'](\n                user.data as User,\n                comment,\n              )}\n            >\n              <div className=\"flex justify-between\">\n                <div>\n                  <span className=\"text-xs font-semibold\">\n                    {formatDate(comment.createdAt)}\n                  </span>\n                  {comment.author && (\n                    <span className=\"text-xs font-bold\">\n                      {' '}\n                      by {comment.author.firstName} {comment.author.lastName}\n                    </span>\n                  )}\n                </div>\n                <DeleteComment discussionId={discussionId} id={comment.id} />\n              </div>\n            </Authorization>\n\n            <MDPreview value={comment.body} />\n          </li>\n        ))}\n      </ul>\n      {commentsQuery.hasNextPage && (\n        <div className=\"flex items-center justify-center py-4\">\n          <Button onClick={() => commentsQuery.fetchNextPage()}>\n            {commentsQuery.isFetchingNextPage ? (\n              <Spinner />\n            ) : (\n              'Load More Comments'\n            )}\n          </Button>\n        </div>\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/react-vite/src/features/comments/components/comments.tsx",
    "content": "import { CommentsList } from './comments-list';\nimport { CreateComment } from './create-comment';\n\ntype CommentsProps = {\n  discussionId: string;\n};\n\nexport const Comments = ({ discussionId }: CommentsProps) => {\n  return (\n    <div>\n      <div className=\"mb-4 flex items-center justify-between\">\n        <h3 className=\"text-xl font-bold\">Comments:</h3>\n        <CreateComment discussionId={discussionId} />\n      </div>\n      <CommentsList discussionId={discussionId} />\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/react-vite/src/features/comments/components/create-comment.tsx",
    "content": "import { Plus } from 'lucide-react';\n\nimport { Button } from '@/components/ui/button';\nimport { Form, FormDrawer, Textarea } from '@/components/ui/form';\nimport { useNotifications } from '@/components/ui/notifications';\n\nimport {\n  useCreateComment,\n  createCommentInputSchema,\n} from '../api/create-comment';\n\ntype CreateCommentProps = {\n  discussionId: string;\n};\n\nexport const CreateComment = ({ discussionId }: CreateCommentProps) => {\n  const { addNotification } = useNotifications();\n  const createCommentMutation = useCreateComment({\n    discussionId,\n    mutationConfig: {\n      onSuccess: () => {\n        addNotification({\n          type: 'success',\n          title: 'Comment Created',\n        });\n      },\n    },\n  });\n\n  return (\n    <FormDrawer\n      isDone={createCommentMutation.isSuccess}\n      triggerButton={\n        <Button size=\"sm\" icon={<Plus className=\"size-4\" />}>\n          Create Comment\n        </Button>\n      }\n      title=\"Create Comment\"\n      submitButton={\n        <Button\n          isLoading={createCommentMutation.isPending}\n          form=\"create-comment\"\n          type=\"submit\"\n          size=\"sm\"\n          disabled={createCommentMutation.isPending}\n        >\n          Submit\n        </Button>\n      }\n    >\n      <Form\n        id=\"create-comment\"\n        onSubmit={(values) => {\n          createCommentMutation.mutate({\n            data: values,\n          });\n        }}\n        schema={createCommentInputSchema}\n        options={{\n          defaultValues: {\n            body: '',\n            discussionId: discussionId,\n          },\n        }}\n      >\n        {({ register, formState }) => (\n          <Textarea\n            label=\"Body\"\n            error={formState.errors['body']}\n            registration={register('body')}\n          />\n        )}\n      </Form>\n    </FormDrawer>\n  );\n};\n"
  },
  {
    "path": "apps/react-vite/src/features/comments/components/delete-comment.tsx",
    "content": "import { Trash } from 'lucide-react';\n\nimport { Button } from '@/components/ui/button';\nimport { ConfirmationDialog } from '@/components/ui/dialog';\nimport { useNotifications } from '@/components/ui/notifications';\n\nimport { useDeleteComment } from '../api/delete-comment';\n\ntype DeleteCommentProps = {\n  id: string;\n  discussionId: string;\n};\n\nexport const DeleteComment = ({ id, discussionId }: DeleteCommentProps) => {\n  const { addNotification } = useNotifications();\n  const deleteCommentMutation = useDeleteComment({\n    discussionId,\n    mutationConfig: {\n      onSuccess: () => {\n        addNotification({\n          type: 'success',\n          title: 'Comment Deleted',\n        });\n      },\n    },\n  });\n\n  return (\n    <ConfirmationDialog\n      isDone={deleteCommentMutation.isSuccess}\n      icon=\"danger\"\n      title=\"Delete Comment\"\n      body=\"Are you sure you want to delete this comment?\"\n      triggerButton={\n        <Button\n          variant=\"destructive\"\n          size=\"sm\"\n          icon={<Trash className=\"size-4\" />}\n        >\n          Delete Comment\n        </Button>\n      }\n      confirmButton={\n        <Button\n          isLoading={deleteCommentMutation.isPending}\n          type=\"button\"\n          variant=\"destructive\"\n          onClick={() => deleteCommentMutation.mutate({ commentId: id })}\n        >\n          Delete Comment\n        </Button>\n      }\n    />\n  );\n};\n"
  },
  {
    "path": "apps/react-vite/src/features/discussions/api/create-discussion.ts",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query';\nimport { z } from 'zod';\n\nimport { api } from '@/lib/api-client';\nimport { MutationConfig } from '@/lib/react-query';\nimport { Discussion } from '@/types/api';\n\nimport { getDiscussionsQueryOptions } from './get-discussions';\n\nexport const createDiscussionInputSchema = z.object({\n  title: z.string().min(1, 'Required'),\n  body: z.string().min(1, 'Required'),\n});\n\nexport type CreateDiscussionInput = z.infer<typeof createDiscussionInputSchema>;\n\nexport const createDiscussion = ({\n  data,\n}: {\n  data: CreateDiscussionInput;\n}): Promise<Discussion> => {\n  return api.post(`/discussions`, data);\n};\n\ntype UseCreateDiscussionOptions = {\n  mutationConfig?: MutationConfig<typeof createDiscussion>;\n};\n\nexport const useCreateDiscussion = ({\n  mutationConfig,\n}: UseCreateDiscussionOptions = {}) => {\n  const queryClient = useQueryClient();\n\n  const { onSuccess, ...restConfig } = mutationConfig || {};\n\n  return useMutation({\n    onSuccess: (...args) => {\n      queryClient.invalidateQueries({\n        queryKey: getDiscussionsQueryOptions().queryKey,\n      });\n      onSuccess?.(...args);\n    },\n    ...restConfig,\n    mutationFn: createDiscussion,\n  });\n};\n"
  },
  {
    "path": "apps/react-vite/src/features/discussions/api/delete-discussion.ts",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query';\n\nimport { api } from '@/lib/api-client';\nimport { MutationConfig } from '@/lib/react-query';\n\nimport { getDiscussionsQueryOptions } from './get-discussions';\n\nexport const deleteDiscussion = ({\n  discussionId,\n}: {\n  discussionId: string;\n}) => {\n  return api.delete(`/discussions/${discussionId}`);\n};\n\ntype UseDeleteDiscussionOptions = {\n  mutationConfig?: MutationConfig<typeof deleteDiscussion>;\n};\n\nexport const useDeleteDiscussion = ({\n  mutationConfig,\n}: UseDeleteDiscussionOptions = {}) => {\n  const queryClient = useQueryClient();\n\n  const { onSuccess, ...restConfig } = mutationConfig || {};\n\n  return useMutation({\n    onSuccess: (...args) => {\n      queryClient.invalidateQueries({\n        queryKey: getDiscussionsQueryOptions().queryKey,\n      });\n      onSuccess?.(...args);\n    },\n    ...restConfig,\n    mutationFn: deleteDiscussion,\n  });\n};\n"
  },
  {
    "path": "apps/react-vite/src/features/discussions/api/get-discussion.ts",
    "content": "import { useQuery, queryOptions } from '@tanstack/react-query';\n\nimport { api } from '@/lib/api-client';\nimport { QueryConfig } from '@/lib/react-query';\nimport { Discussion } from '@/types/api';\n\nexport const getDiscussion = ({\n  discussionId,\n}: {\n  discussionId: string;\n}): Promise<{ data: Discussion }> => {\n  return api.get(`/discussions/${discussionId}`);\n};\n\nexport const getDiscussionQueryOptions = (discussionId: string) => {\n  return queryOptions({\n    queryKey: ['discussions', discussionId],\n    queryFn: () => getDiscussion({ discussionId }),\n  });\n};\n\ntype UseDiscussionOptions = {\n  discussionId: string;\n  queryConfig?: QueryConfig<typeof getDiscussionQueryOptions>;\n};\n\nexport const useDiscussion = ({\n  discussionId,\n  queryConfig,\n}: UseDiscussionOptions) => {\n  return useQuery({\n    ...getDiscussionQueryOptions(discussionId),\n    ...queryConfig,\n  });\n};\n"
  },
  {
    "path": "apps/react-vite/src/features/discussions/api/get-discussions.ts",
    "content": "import { queryOptions, useQuery } from '@tanstack/react-query';\n\nimport { api } from '@/lib/api-client';\nimport { QueryConfig } from '@/lib/react-query';\nimport { Discussion, Meta } from '@/types/api';\n\nexport const getDiscussions = (\n  page = 1,\n): Promise<{\n  data: Discussion[];\n  meta: Meta;\n}> => {\n  return api.get(`/discussions`, {\n    params: {\n      page,\n    },\n  });\n};\n\nexport const getDiscussionsQueryOptions = ({\n  page,\n}: { page?: number } = {}) => {\n  return queryOptions({\n    queryKey: page ? ['discussions', { page }] : ['discussions'],\n    queryFn: () => getDiscussions(page),\n  });\n};\n\ntype UseDiscussionsOptions = {\n  page?: number;\n  queryConfig?: QueryConfig<typeof getDiscussionsQueryOptions>;\n};\n\nexport const useDiscussions = ({\n  queryConfig,\n  page,\n}: UseDiscussionsOptions) => {\n  return useQuery({\n    ...getDiscussionsQueryOptions({ page }),\n    ...queryConfig,\n  });\n};\n"
  },
  {
    "path": "apps/react-vite/src/features/discussions/api/update-discussion.ts",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query';\nimport { z } from 'zod';\n\nimport { api } from '@/lib/api-client';\nimport { MutationConfig } from '@/lib/react-query';\nimport { Discussion } from '@/types/api';\n\nimport { getDiscussionQueryOptions } from './get-discussion';\n\nexport const updateDiscussionInputSchema = z.object({\n  title: z.string().min(1, 'Required'),\n  body: z.string().min(1, 'Required'),\n});\n\nexport type UpdateDiscussionInput = z.infer<typeof updateDiscussionInputSchema>;\n\nexport const updateDiscussion = ({\n  data,\n  discussionId,\n}: {\n  data: UpdateDiscussionInput;\n  discussionId: string;\n}): Promise<Discussion> => {\n  return api.patch(`/discussions/${discussionId}`, data);\n};\n\ntype UseUpdateDiscussionOptions = {\n  mutationConfig?: MutationConfig<typeof updateDiscussion>;\n};\n\nexport const useUpdateDiscussion = ({\n  mutationConfig,\n}: UseUpdateDiscussionOptions = {}) => {\n  const queryClient = useQueryClient();\n\n  const { onSuccess, ...restConfig } = mutationConfig || {};\n\n  return useMutation({\n    onSuccess: (data, ...args) => {\n      queryClient.refetchQueries({\n        queryKey: getDiscussionQueryOptions(data.id).queryKey,\n      });\n      onSuccess?.(data, ...args);\n    },\n    ...restConfig,\n    mutationFn: updateDiscussion,\n  });\n};\n"
  },
  {
    "path": "apps/react-vite/src/features/discussions/components/create-discussion.tsx",
    "content": "import { Plus } from 'lucide-react';\n\nimport { Button } from '@/components/ui/button';\nimport { Form, FormDrawer, Input, Textarea } from '@/components/ui/form';\nimport { useNotifications } from '@/components/ui/notifications';\nimport { Authorization, ROLES } from '@/lib/authorization';\n\nimport {\n  createDiscussionInputSchema,\n  useCreateDiscussion,\n} from '../api/create-discussion';\n\nexport const CreateDiscussion = () => {\n  const { addNotification } = useNotifications();\n  const createDiscussionMutation = useCreateDiscussion({\n    mutationConfig: {\n      onSuccess: () => {\n        addNotification({\n          type: 'success',\n          title: 'Discussion Created',\n        });\n      },\n    },\n  });\n\n  return (\n    <Authorization allowedRoles={[ROLES.ADMIN]}>\n      <FormDrawer\n        isDone={createDiscussionMutation.isSuccess}\n        triggerButton={\n          <Button size=\"sm\" icon={<Plus className=\"size-4\" />}>\n            Create Discussion\n          </Button>\n        }\n        title=\"Create Discussion\"\n        submitButton={\n          <Button\n            form=\"create-discussion\"\n            type=\"submit\"\n            size=\"sm\"\n            isLoading={createDiscussionMutation.isPending}\n          >\n            Submit\n          </Button>\n        }\n      >\n        <Form\n          id=\"create-discussion\"\n          onSubmit={(values) => {\n            createDiscussionMutation.mutate({ data: values });\n          }}\n          schema={createDiscussionInputSchema}\n        >\n          {({ register, formState }) => (\n            <>\n              <Input\n                label=\"Title\"\n                error={formState.errors['title']}\n                registration={register('title')}\n              />\n\n              <Textarea\n                label=\"Body\"\n                error={formState.errors['body']}\n                registration={register('body')}\n              />\n            </>\n          )}\n        </Form>\n      </FormDrawer>\n    </Authorization>\n  );\n};\n"
  },
  {
    "path": "apps/react-vite/src/features/discussions/components/delete-discussion.tsx",
    "content": "import { Trash } from 'lucide-react';\n\nimport { Button } from '@/components/ui/button';\nimport { ConfirmationDialog } from '@/components/ui/dialog';\nimport { useNotifications } from '@/components/ui/notifications';\nimport { Authorization, ROLES } from '@/lib/authorization';\n\nimport { useDeleteDiscussion } from '../api/delete-discussion';\n\ntype DeleteDiscussionProps = {\n  id: string;\n};\n\nexport const DeleteDiscussion = ({ id }: DeleteDiscussionProps) => {\n  const { addNotification } = useNotifications();\n  const deleteDiscussionMutation = useDeleteDiscussion({\n    mutationConfig: {\n      onSuccess: () => {\n        addNotification({\n          type: 'success',\n          title: 'Discussion Deleted',\n        });\n      },\n    },\n  });\n\n  return (\n    <Authorization allowedRoles={[ROLES.ADMIN]}>\n      <ConfirmationDialog\n        icon=\"danger\"\n        title=\"Delete Discussion\"\n        body=\"Are you sure you want to delete this discussion?\"\n        triggerButton={\n          <Button variant=\"destructive\" icon={<Trash className=\"size-4\" />}>\n            Delete Discussion\n          </Button>\n        }\n        confirmButton={\n          <Button\n            isLoading={deleteDiscussionMutation.isPending}\n            type=\"button\"\n            variant=\"destructive\"\n            onClick={() =>\n              deleteDiscussionMutation.mutate({ discussionId: id })\n            }\n          >\n            Delete Discussion\n          </Button>\n        }\n      />\n    </Authorization>\n  );\n};\n"
  },
  {
    "path": "apps/react-vite/src/features/discussions/components/discussion-view.tsx",
    "content": "import { MDPreview } from '@/components/ui/md-preview';\nimport { Spinner } from '@/components/ui/spinner';\nimport { formatDate } from '@/utils/format';\n\nimport { useDiscussion } from '../api/get-discussion';\nimport { UpdateDiscussion } from '../components/update-discussion';\n\nexport const DiscussionView = ({ discussionId }: { discussionId: string }) => {\n  const discussionQuery = useDiscussion({\n    discussionId,\n  });\n\n  if (discussionQuery.isLoading) {\n    return (\n      <div className=\"flex h-48 w-full items-center justify-center\">\n        <Spinner size=\"lg\" />\n      </div>\n    );\n  }\n\n  const discussion = discussionQuery?.data?.data;\n\n  if (!discussion) return null;\n\n  return (\n    <div>\n      <span className=\"text-xs font-bold\">\n        {formatDate(discussion.createdAt)}\n      </span>\n      {discussion.author && (\n        <span className=\"ml-2 text-sm font-bold\">\n          by {discussion.author.firstName} {discussion.author.lastName}\n        </span>\n      )}\n      <div className=\"mt-6 flex flex-col space-y-16\">\n        <div className=\"flex justify-end\">\n          <UpdateDiscussion discussionId={discussionId} />\n        </div>\n        <div>\n          <div className=\"overflow-hidden bg-white shadow sm:rounded-lg\">\n            <div className=\"px-4 py-5 sm:px-6\">\n              <div className=\"mt-1 max-w-2xl text-sm text-gray-500\">\n                <MDPreview value={discussion.body} />\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/react-vite/src/features/discussions/components/discussions-list.tsx",
    "content": "import { useQueryClient } from '@tanstack/react-query';\nimport { useSearchParams } from 'react-router';\n\nimport { Link } from '@/components/ui/link';\nimport { Spinner } from '@/components/ui/spinner';\nimport { Table } from '@/components/ui/table';\nimport { paths } from '@/config/paths';\nimport { formatDate } from '@/utils/format';\n\nimport { getDiscussionQueryOptions } from '../api/get-discussion';\nimport { useDiscussions } from '../api/get-discussions';\n\nimport { DeleteDiscussion } from './delete-discussion';\n\nexport type DiscussionsListProps = {\n  onDiscussionPrefetch?: (id: string) => void;\n};\n\nexport const DiscussionsList = ({\n  onDiscussionPrefetch,\n}: DiscussionsListProps) => {\n  const [searchParams] = useSearchParams();\n\n  const discussionsQuery = useDiscussions({\n    page: +(searchParams.get('page') || 1),\n  });\n  const queryClient = useQueryClient();\n\n  if (discussionsQuery.isLoading) {\n    return (\n      <div className=\"flex h-48 w-full items-center justify-center\">\n        <Spinner size=\"lg\" />\n      </div>\n    );\n  }\n\n  const discussions = discussionsQuery.data?.data;\n  const meta = discussionsQuery.data?.meta;\n\n  if (!discussions) return null;\n\n  return (\n    <Table\n      data={discussions}\n      columns={[\n        {\n          title: 'Title',\n          field: 'title',\n        },\n        {\n          title: 'Created At',\n          field: 'createdAt',\n          Cell({ entry: { createdAt } }) {\n            return <span>{formatDate(createdAt)}</span>;\n          },\n        },\n        {\n          title: '',\n          field: 'id',\n          Cell({ entry: { id } }) {\n            return (\n              <Link\n                onMouseEnter={() => {\n                  // Prefetch the discussion data when the user hovers over the link\n                  queryClient.prefetchQuery(getDiscussionQueryOptions(id));\n                  onDiscussionPrefetch?.(id);\n                }}\n                to={paths.app.discussion.getHref(id)}\n              >\n                View\n              </Link>\n            );\n          },\n        },\n        {\n          title: '',\n          field: 'id',\n          Cell({ entry: { id } }) {\n            return <DeleteDiscussion id={id} />;\n          },\n        },\n      ]}\n      pagination={\n        meta && {\n          totalPages: meta.totalPages,\n          currentPage: meta.page,\n          rootUrl: '',\n        }\n      }\n    />\n  );\n};\n"
  },
  {
    "path": "apps/react-vite/src/features/discussions/components/update-discussion.tsx",
    "content": "import { Pen } from 'lucide-react';\n\nimport { Button } from '@/components/ui/button';\nimport { Form, FormDrawer, Input, Textarea } from '@/components/ui/form';\nimport { useNotifications } from '@/components/ui/notifications';\nimport { Authorization, ROLES } from '@/lib/authorization';\n\nimport { useDiscussion } from '../api/get-discussion';\nimport {\n  updateDiscussionInputSchema,\n  useUpdateDiscussion,\n} from '../api/update-discussion';\n\ntype UpdateDiscussionProps = {\n  discussionId: string;\n};\n\nexport const UpdateDiscussion = ({ discussionId }: UpdateDiscussionProps) => {\n  const { addNotification } = useNotifications();\n  const discussionQuery = useDiscussion({ discussionId });\n  const updateDiscussionMutation = useUpdateDiscussion({\n    mutationConfig: {\n      onSuccess: () => {\n        addNotification({\n          type: 'success',\n          title: 'Discussion Updated',\n        });\n      },\n    },\n  });\n\n  const discussion = discussionQuery.data?.data;\n\n  return (\n    <Authorization allowedRoles={[ROLES.ADMIN]}>\n      <FormDrawer\n        isDone={updateDiscussionMutation.isSuccess}\n        triggerButton={\n          <Button icon={<Pen className=\"size-4\" />} size=\"sm\">\n            Update Discussion\n          </Button>\n        }\n        title=\"Update Discussion\"\n        submitButton={\n          <Button\n            form=\"update-discussion\"\n            type=\"submit\"\n            size=\"sm\"\n            isLoading={updateDiscussionMutation.isPending}\n          >\n            Submit\n          </Button>\n        }\n      >\n        <Form\n          id=\"update-discussion\"\n          onSubmit={(values) => {\n            updateDiscussionMutation.mutate({\n              data: values,\n              discussionId,\n            });\n          }}\n          options={{\n            defaultValues: {\n              title: discussion?.title ?? '',\n              body: discussion?.body ?? '',\n            },\n          }}\n          schema={updateDiscussionInputSchema}\n        >\n          {({ register, formState }) => (\n            <>\n              <Input\n                label=\"Title\"\n                error={formState.errors['title']}\n                registration={register('title')}\n              />\n              <Textarea\n                label=\"Body\"\n                error={formState.errors['body']}\n                registration={register('body')}\n              />\n            </>\n          )}\n        </Form>\n      </FormDrawer>\n    </Authorization>\n  );\n};\n"
  },
  {
    "path": "apps/react-vite/src/features/teams/api/get-teams.ts",
    "content": "import { queryOptions, useQuery } from '@tanstack/react-query';\n\nimport { api } from '@/lib/api-client';\nimport { QueryConfig } from '@/lib/react-query';\nimport { Team } from '@/types/api';\n\nexport const getTeams = (): Promise<{ data: Team[] }> => {\n  return api.get('/teams');\n};\n\nexport const getTeamsQueryOptions = () => {\n  return queryOptions({\n    queryKey: ['teams'],\n    queryFn: () => getTeams(),\n  });\n};\n\ntype UseTeamsOptions = {\n  queryConfig?: QueryConfig<typeof getTeamsQueryOptions>;\n};\n\nexport const useTeams = ({ queryConfig = {} }: UseTeamsOptions = {}) => {\n  return useQuery({\n    ...getTeamsQueryOptions(),\n    ...queryConfig,\n  });\n};\n"
  },
  {
    "path": "apps/react-vite/src/features/users/api/delete-user.ts",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query';\n\nimport { api } from '@/lib/api-client';\nimport { MutationConfig } from '@/lib/react-query';\n\nimport { getUsersQueryOptions } from './get-users';\n\nexport type DeleteUserDTO = {\n  userId: string;\n};\n\nexport const deleteUser = ({ userId }: DeleteUserDTO) => {\n  return api.delete(`/users/${userId}`);\n};\n\ntype UseDeleteUserOptions = {\n  mutationConfig?: MutationConfig<typeof deleteUser>;\n};\n\nexport const useDeleteUser = ({\n  mutationConfig,\n}: UseDeleteUserOptions = {}) => {\n  const queryClient = useQueryClient();\n\n  const { onSuccess, ...restConfig } = mutationConfig || {};\n\n  return useMutation({\n    onSuccess: (...args) => {\n      queryClient.invalidateQueries({\n        queryKey: getUsersQueryOptions().queryKey,\n      });\n      onSuccess?.(...args);\n    },\n    ...restConfig,\n    mutationFn: deleteUser,\n  });\n};\n"
  },
  {
    "path": "apps/react-vite/src/features/users/api/get-users.ts",
    "content": "import { queryOptions, useQuery } from '@tanstack/react-query';\n\nimport { api } from '@/lib/api-client';\nimport { QueryConfig } from '@/lib/react-query';\nimport { User } from '@/types/api';\n\nexport const getUsers = (): Promise<{ data: User[] }> => {\n  return api.get(`/users`);\n};\n\nexport const getUsersQueryOptions = () => {\n  return queryOptions({\n    queryKey: ['users'],\n    queryFn: getUsers,\n  });\n};\n\ntype UseUsersOptions = {\n  queryConfig?: QueryConfig<typeof getUsersQueryOptions>;\n};\n\nexport const useUsers = ({ queryConfig }: UseUsersOptions = {}) => {\n  return useQuery({\n    ...getUsersQueryOptions(),\n    ...queryConfig,\n  });\n};\n"
  },
  {
    "path": "apps/react-vite/src/features/users/api/update-profile.ts",
    "content": "import { useMutation } from '@tanstack/react-query';\nimport { z } from 'zod';\n\nimport { api } from '@/lib/api-client';\nimport { useUser } from '@/lib/auth';\nimport { MutationConfig } from '@/lib/react-query';\n\nexport const updateProfileInputSchema = z.object({\n  email: z.string().min(1, 'Required').email('Invalid email'),\n  firstName: z.string().min(1, 'Required'),\n  lastName: z.string().min(1, 'Required'),\n  bio: z.string(),\n});\n\nexport type UpdateProfileInput = z.infer<typeof updateProfileInputSchema>;\n\nexport const updateProfile = ({ data }: { data: UpdateProfileInput }) => {\n  return api.patch(`/users/profile`, data);\n};\n\ntype UseUpdateProfileOptions = {\n  mutationConfig?: MutationConfig<typeof updateProfile>;\n};\n\nexport const useUpdateProfile = ({\n  mutationConfig,\n}: UseUpdateProfileOptions = {}) => {\n  const { refetch: refetchUser } = useUser();\n\n  const { onSuccess, ...restConfig } = mutationConfig || {};\n\n  return useMutation({\n    onSuccess: (...args) => {\n      refetchUser();\n      onSuccess?.(...args);\n    },\n    ...restConfig,\n    mutationFn: updateProfile,\n  });\n};\n"
  },
  {
    "path": "apps/react-vite/src/features/users/components/delete-user.tsx",
    "content": "import { Button } from '@/components/ui/button';\nimport { ConfirmationDialog } from '@/components/ui/dialog';\nimport { useNotifications } from '@/components/ui/notifications';\nimport { useUser } from '@/lib/auth';\n\nimport { useDeleteUser } from '../api/delete-user';\n\ntype DeleteUserProps = {\n  id: string;\n};\n\nexport const DeleteUser = ({ id }: DeleteUserProps) => {\n  const user = useUser();\n  const { addNotification } = useNotifications();\n  const deleteUserMutation = useDeleteUser({\n    mutationConfig: {\n      onSuccess: () => {\n        addNotification({\n          type: 'success',\n          title: 'User Deleted',\n        });\n      },\n    },\n  });\n\n  if (user.data?.id === id) return null;\n\n  return (\n    <ConfirmationDialog\n      icon=\"danger\"\n      title=\"Delete User\"\n      body=\"Are you sure you want to delete this user?\"\n      triggerButton={<Button variant=\"destructive\">Delete</Button>}\n      confirmButton={\n        <Button\n          isLoading={deleteUserMutation.isPending}\n          type=\"button\"\n          variant=\"destructive\"\n          onClick={() => deleteUserMutation.mutate({ userId: id })}\n        >\n          Delete User\n        </Button>\n      }\n    />\n  );\n};\n"
  },
  {
    "path": "apps/react-vite/src/features/users/components/update-profile.tsx",
    "content": "import { Pen } from 'lucide-react';\n\nimport { Button } from '@/components/ui/button';\nimport { Form, FormDrawer, Input, Textarea } from '@/components/ui/form';\nimport { useNotifications } from '@/components/ui/notifications';\nimport { useUser } from '@/lib/auth';\n\nimport {\n  updateProfileInputSchema,\n  useUpdateProfile,\n} from '../api/update-profile';\n\nexport const UpdateProfile = () => {\n  const user = useUser();\n  const { addNotification } = useNotifications();\n  const updateProfileMutation = useUpdateProfile({\n    mutationConfig: {\n      onSuccess: () => {\n        addNotification({\n          type: 'success',\n          title: 'Profile Updated',\n        });\n      },\n    },\n  });\n\n  return (\n    <FormDrawer\n      isDone={updateProfileMutation.isSuccess}\n      triggerButton={\n        <Button icon={<Pen className=\"size-4\" />} size=\"sm\">\n          Update Profile\n        </Button>\n      }\n      title=\"Update Profile\"\n      submitButton={\n        <Button\n          form=\"update-profile\"\n          type=\"submit\"\n          size=\"sm\"\n          isLoading={updateProfileMutation.isPending}\n        >\n          Submit\n        </Button>\n      }\n    >\n      <Form\n        id=\"update-profile\"\n        onSubmit={(values) => {\n          updateProfileMutation.mutate({ data: values });\n        }}\n        options={{\n          defaultValues: {\n            firstName: user.data?.firstName ?? '',\n            lastName: user.data?.lastName ?? '',\n            email: user.data?.email ?? '',\n            bio: user.data?.bio ?? '',\n          },\n        }}\n        schema={updateProfileInputSchema}\n      >\n        {({ register, formState }) => (\n          <>\n            <Input\n              label=\"First Name\"\n              error={formState.errors['firstName']}\n              registration={register('firstName')}\n            />\n            <Input\n              label=\"Last Name\"\n              error={formState.errors['lastName']}\n              registration={register('lastName')}\n            />\n            <Input\n              label=\"Email Address\"\n              type=\"email\"\n              error={formState.errors['email']}\n              registration={register('email')}\n            />\n\n            <Textarea\n              label=\"Bio\"\n              error={formState.errors['bio']}\n              registration={register('bio')}\n            />\n          </>\n        )}\n      </Form>\n    </FormDrawer>\n  );\n};\n"
  },
  {
    "path": "apps/react-vite/src/features/users/components/users-list.tsx",
    "content": "import { Spinner } from '@/components/ui/spinner';\nimport { Table } from '@/components/ui/table';\nimport { formatDate } from '@/utils/format';\n\nimport { useUsers } from '../api/get-users';\n\nimport { DeleteUser } from './delete-user';\n\nexport const UsersList = () => {\n  const usersQuery = useUsers();\n\n  if (usersQuery.isLoading) {\n    return (\n      <div className=\"flex h-48 w-full items-center justify-center\">\n        <Spinner size=\"lg\" />\n      </div>\n    );\n  }\n\n  const users = usersQuery.data?.data;\n\n  if (!users) return null;\n\n  return (\n    <Table\n      data={users}\n      columns={[\n        {\n          title: 'First Name',\n          field: 'firstName',\n        },\n        {\n          title: 'Last Name',\n          field: 'lastName',\n        },\n        {\n          title: 'Email',\n          field: 'email',\n        },\n        {\n          title: 'Role',\n          field: 'role',\n        },\n        {\n          title: 'Created At',\n          field: 'createdAt',\n          Cell({ entry: { createdAt } }) {\n            return <span>{formatDate(createdAt)}</span>;\n          },\n        },\n        {\n          title: '',\n          field: 'id',\n          Cell({ entry: { id } }) {\n            return <DeleteUser id={id} />;\n          },\n        },\n      ]}\n    />\n  );\n};\n"
  },
  {
    "path": "apps/react-vite/src/hooks/__tests__/use-disclosure.test.ts",
    "content": "import { renderHook, act } from '@testing-library/react';\n\nimport { useDisclosure } from '../use-disclosure';\n\ntest('should open the state', () => {\n  const { result } = renderHook(() => useDisclosure());\n\n  expect(result.current.isOpen).toBe(false);\n\n  act(() => {\n    result.current.open();\n  });\n\n  expect(result.current.isOpen).toBe(true);\n});\n\ntest('should close the state', () => {\n  const { result } = renderHook(() => useDisclosure());\n\n  expect(result.current.isOpen).toBe(false);\n\n  act(() => {\n    result.current.close();\n  });\n\n  expect(result.current.isOpen).toBe(false);\n});\n\ntest('should toggle the state', () => {\n  const { result } = renderHook(() => useDisclosure());\n\n  expect(result.current.isOpen).toBe(false);\n\n  act(() => {\n    result.current.toggle();\n  });\n\n  expect(result.current.isOpen).toBe(true);\n\n  act(() => {\n    result.current.toggle();\n  });\n\n  expect(result.current.isOpen).toBe(false);\n});\n\ntest('should define initial state', () => {\n  const { result } = renderHook(() => useDisclosure(true));\n\n  expect(result.current.isOpen).toBe(true);\n\n  act(() => {\n    result.current.toggle();\n  });\n\n  expect(result.current.isOpen).toBe(false);\n});\n"
  },
  {
    "path": "apps/react-vite/src/hooks/use-disclosure.ts",
    "content": "import * as React from 'react';\n\nexport const useDisclosure = (initial = false) => {\n  const [isOpen, setIsOpen] = React.useState(initial);\n\n  const open = React.useCallback(() => setIsOpen(true), []);\n  const close = React.useCallback(() => setIsOpen(false), []);\n  const toggle = React.useCallback(() => setIsOpen((state) => !state), []);\n\n  return { isOpen, open, close, toggle };\n};\n"
  },
  {
    "path": "apps/react-vite/src/index.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer base {\n  :root {\n    --background: 0 0% 100%;\n    --foreground: 222.2 84% 4.9%;\n\n    --card: 0 0% 100%;\n    --card-foreground: 222.2 84% 4.9%;\n\n    --popover: 0 0% 100%;\n    --popover-foreground: 222.2 84% 4.9%;\n\n    --primary: 222.2 47.4% 11.2%;\n    --primary-foreground: 210 40% 98%;\n\n    --secondary: 210 40% 96.1%;\n    --secondary-foreground: 222.2 47.4% 11.2%;\n\n    --muted: 210 40% 96.1%;\n    --muted-foreground: 215.4 16.3% 46.9%;\n\n    --accent: 210 40% 96.1%;\n    --accent-foreground: 222.2 47.4% 11.2%;\n\n    --destructive: 0 84.2% 60.2%;\n    --destructive-foreground: 210 40% 98%;\n\n    --border: 214.3 31.8% 91.4%;\n    --input: 214.3 31.8% 91.4%;\n    --ring: 222.2 84% 4.9%;\n\n    --radius: 0.5rem;\n  }\n\n  .dark {\n    --background: 222.2 84% 4.9%;\n    --foreground: 210 40% 98%;\n\n    --card: 222.2 84% 4.9%;\n    --card-foreground: 210 40% 98%;\n\n    --popover: 222.2 84% 4.9%;\n    --popover-foreground: 210 40% 98%;\n\n    --primary: 210 40% 98%;\n    --primary-foreground: 222.2 47.4% 11.2%;\n\n    --secondary: 217.2 32.6% 17.5%;\n    --secondary-foreground: 210 40% 98%;\n\n    --muted: 217.2 32.6% 17.5%;\n    --muted-foreground: 215 20.2% 65.1%;\n\n    --accent: 217.2 32.6% 17.5%;\n    --accent-foreground: 210 40% 98%;\n\n    --destructive: 0 62.8% 30.6%;\n    --destructive-foreground: 210 40% 98%;\n\n    --border: 217.2 32.6% 17.5%;\n    --input: 217.2 32.6% 17.5%;\n    --ring: 212.7 26.8% 83.9%;\n  }\n}\n\n@layer base {\n  * {\n    @apply border-border;\n  }\n  body {\n    @apply bg-background text-foreground;\n  }\n}\n\nbody {\n  margin: 0;\n  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',\n    'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;\n}\n"
  },
  {
    "path": "apps/react-vite/src/lib/__tests__/authorization.test.tsx",
    "content": "import { createUser, renderApp, screen } from '@/testing/test-utils';\n\nimport { Authorization, ROLES } from '../authorization';\n\ntest('should view protected resource if user role is matching', async () => {\n  const user = await createUser({\n    role: ROLES.ADMIN,\n  });\n\n  const protectedResource = 'This is very confidential data';\n\n  await renderApp(\n    <Authorization allowedRoles={[ROLES.ADMIN]}>\n      {protectedResource}\n    </Authorization>,\n    {\n      user,\n    },\n  );\n\n  expect(screen.getByText(protectedResource)).toBeInTheDocument();\n});\n\ntest('should not view protected resource if user role does not match and show fallback message instead', async () => {\n  const user = await createUser({\n    role: ROLES.USER,\n  });\n\n  const protectedResource = 'This is very confidential data';\n\n  const forbiddenMessage = 'You are unauthorized to view this resource';\n  await renderApp(\n    <Authorization\n      forbiddenFallback={<div>{forbiddenMessage}</div>}\n      allowedRoles={[ROLES.ADMIN]}\n    >\n      {protectedResource}\n    </Authorization>,\n    { user },\n  );\n\n  expect(screen.queryByText(protectedResource)).not.toBeInTheDocument();\n\n  expect(screen.getByText(forbiddenMessage)).toBeInTheDocument();\n});\n\ntest('should view protected resource if policy check passes', async () => {\n  const user = await createUser({\n    role: ROLES.ADMIN,\n  });\n\n  const protectedResource = 'This is very confidential data';\n\n  await renderApp(\n    <Authorization policyCheck={true}>{protectedResource}</Authorization>,\n    { user },\n  );\n\n  expect(screen.getByText(protectedResource)).toBeInTheDocument();\n});\n\ntest('should not view protected resource if policy check fails and show fallback message instead', async () => {\n  const user = await createUser({\n    role: ROLES.USER,\n  });\n\n  const protectedResource = 'This is very confidential data';\n\n  const forbiddenMessage = 'You are unauthorized to view this resource';\n  await renderApp(\n    <Authorization\n      forbiddenFallback={<div>{forbiddenMessage}</div>}\n      policyCheck={false}\n    >\n      {protectedResource}\n    </Authorization>,\n    { user },\n  );\n\n  expect(screen.queryByText(protectedResource)).not.toBeInTheDocument();\n\n  expect(screen.getByText(forbiddenMessage)).toBeInTheDocument();\n});\n"
  },
  {
    "path": "apps/react-vite/src/lib/api-client.ts",
    "content": "import Axios, { InternalAxiosRequestConfig } from 'axios';\n\nimport { useNotifications } from '@/components/ui/notifications';\nimport { env } from '@/config/env';\nimport { paths } from '@/config/paths';\n\nfunction authRequestInterceptor(config: InternalAxiosRequestConfig) {\n  if (config.headers) {\n    config.headers.Accept = 'application/json';\n  }\n\n  config.withCredentials = true;\n  return config;\n}\n\nexport const api = Axios.create({\n  baseURL: env.API_URL,\n});\n\napi.interceptors.request.use(authRequestInterceptor);\napi.interceptors.response.use(\n  (response) => {\n    return response.data;\n  },\n  (error) => {\n    const message = error.response?.data?.message || error.message;\n    useNotifications.getState().addNotification({\n      type: 'error',\n      title: 'Error',\n      message,\n    });\n\n    if (error.response?.status === 401) {\n      const searchParams = new URLSearchParams();\n      const redirectTo =\n        searchParams.get('redirectTo') || window.location.pathname;\n      window.location.href = paths.auth.login.getHref(redirectTo);\n    }\n\n    return Promise.reject(error);\n  },\n);\n"
  },
  {
    "path": "apps/react-vite/src/lib/auth.tsx",
    "content": "import { configureAuth } from 'react-query-auth';\nimport { Navigate, useLocation } from 'react-router';\nimport { z } from 'zod';\n\nimport { paths } from '@/config/paths';\nimport { AuthResponse, User } from '@/types/api';\n\nimport { api } from './api-client';\n\n// api call definitions for auth (types, schemas, requests):\n// these are not part of features as this is a module shared across features\n\nconst getUser = async (): Promise<User> => {\n  const response = await api.get('/auth/me');\n\n  return response.data;\n};\n\nconst logout = (): Promise<void> => {\n  return api.post('/auth/logout');\n};\n\nexport const loginInputSchema = z.object({\n  email: z.string().min(1, 'Required').email('Invalid email'),\n  password: z.string().min(5, 'Required'),\n});\n\nexport type LoginInput = z.infer<typeof loginInputSchema>;\nconst loginWithEmailAndPassword = (data: LoginInput): Promise<AuthResponse> => {\n  return api.post('/auth/login', data);\n};\n\nexport const registerInputSchema = z\n  .object({\n    email: z.string().min(1, 'Required'),\n    firstName: z.string().min(1, 'Required'),\n    lastName: z.string().min(1, 'Required'),\n    password: z.string().min(5, 'Required'),\n  })\n  .and(\n    z\n      .object({\n        teamId: z.string().min(1, 'Required'),\n        teamName: z.null().default(null),\n      })\n      .or(\n        z.object({\n          teamName: z.string().min(1, 'Required'),\n          teamId: z.null().default(null),\n        }),\n      ),\n  );\n\nexport type RegisterInput = z.infer<typeof registerInputSchema>;\n\nconst registerWithEmailAndPassword = (\n  data: RegisterInput,\n): Promise<AuthResponse> => {\n  return api.post('/auth/register', data);\n};\n\nconst authConfig = {\n  userFn: getUser,\n  loginFn: async (data: LoginInput) => {\n    const response = await loginWithEmailAndPassword(data);\n    return response.user;\n  },\n  registerFn: async (data: RegisterInput) => {\n    const response = await registerWithEmailAndPassword(data);\n    return response.user;\n  },\n  logoutFn: logout,\n};\n\nexport const { useUser, useLogin, useLogout, useRegister, AuthLoader } =\n  configureAuth(authConfig);\n\nexport const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {\n  const user = useUser();\n  const location = useLocation();\n\n  if (!user.data) {\n    return (\n      <Navigate to={paths.auth.login.getHref(location.pathname)} replace />\n    );\n  }\n\n  return children;\n};\n"
  },
  {
    "path": "apps/react-vite/src/lib/authorization.tsx",
    "content": "import * as React from 'react';\n\nimport { Comment, User } from '@/types/api';\n\nimport { useUser } from './auth';\n\nexport enum ROLES {\n  ADMIN = 'ADMIN',\n  USER = 'USER',\n}\n\ntype RoleTypes = keyof typeof ROLES;\n\nexport const POLICIES = {\n  'comment:delete': (user: User, comment: Comment) => {\n    if (user.role === 'ADMIN') {\n      return true;\n    }\n\n    if (user.role === 'USER' && comment.author?.id === user.id) {\n      return true;\n    }\n\n    return false;\n  },\n};\n\nexport const useAuthorization = () => {\n  const user = useUser();\n\n  if (!user.data) {\n    throw Error('User does not exist!');\n  }\n\n  const checkAccess = React.useCallback(\n    ({ allowedRoles }: { allowedRoles: RoleTypes[] }) => {\n      if (allowedRoles && allowedRoles.length > 0 && user.data) {\n        return allowedRoles?.includes(user.data.role);\n      }\n\n      return true;\n    },\n    [user.data],\n  );\n\n  return { checkAccess, role: user.data.role };\n};\n\ntype AuthorizationProps = {\n  forbiddenFallback?: React.ReactNode;\n  children: React.ReactNode;\n} & (\n  | {\n      allowedRoles: RoleTypes[];\n      policyCheck?: never;\n    }\n  | {\n      allowedRoles?: never;\n      policyCheck: boolean;\n    }\n);\n\nexport const Authorization = ({\n  policyCheck,\n  allowedRoles,\n  forbiddenFallback = null,\n  children,\n}: AuthorizationProps) => {\n  const { checkAccess } = useAuthorization();\n\n  let canAccess = false;\n\n  if (allowedRoles) {\n    canAccess = checkAccess({ allowedRoles });\n  }\n\n  if (typeof policyCheck !== 'undefined') {\n    canAccess = policyCheck;\n  }\n\n  return <>{canAccess ? children : forbiddenFallback}</>;\n};\n"
  },
  {
    "path": "apps/react-vite/src/lib/react-query.ts",
    "content": "import { UseMutationOptions, DefaultOptions } from '@tanstack/react-query';\n\nexport const queryConfig = {\n  queries: {\n    // throwOnError: true,\n    refetchOnWindowFocus: false,\n    retry: false,\n    staleTime: 1000 * 60,\n  },\n} satisfies DefaultOptions;\n\nexport type ApiFnReturnType<FnType extends (...args: any) => Promise<any>> =\n  Awaited<ReturnType<FnType>>;\n\nexport type QueryConfig<T extends (...args: any[]) => any> = Omit<\n  ReturnType<T>,\n  'queryKey' | 'queryFn'\n>;\n\nexport type MutationConfig<\n  MutationFnType extends (...args: any) => Promise<any>,\n> = UseMutationOptions<\n  ApiFnReturnType<MutationFnType>,\n  Error,\n  Parameters<MutationFnType>[0]\n>;\n"
  },
  {
    "path": "apps/react-vite/src/main.tsx",
    "content": "import * as React from 'react';\nimport { createRoot } from 'react-dom/client';\n\nimport './index.css';\nimport { App } from './app';\nimport { enableMocking } from './testing/mocks';\n\nconst root = document.getElementById('root');\nif (!root) throw new Error('No root element found');\n\nenableMocking().then(() => {\n  createRoot(root).render(\n    <React.StrictMode>\n      <App />\n    </React.StrictMode>,\n  );\n});\n"
  },
  {
    "path": "apps/react-vite/src/testing/data-generators.ts",
    "content": "import {\n  randCompanyName,\n  randUserName,\n  randEmail,\n  randParagraph,\n  randUuid,\n  randPassword,\n  randCatchPhrase,\n} from '@ngneat/falso';\n\nconst generateUser = () => ({\n  id: randUuid() + Math.random(),\n  firstName: randUserName({ withAccents: false }),\n  lastName: randUserName({ withAccents: false }),\n  email: randEmail(),\n  password: randPassword(),\n  teamId: randUuid(),\n  teamName: randCompanyName(),\n  role: 'ADMIN',\n  bio: randParagraph(),\n  createdAt: Date.now(),\n});\n\nexport const createUser = <T extends Partial<ReturnType<typeof generateUser>>>(\n  overrides?: T,\n) => {\n  return { ...generateUser(), ...overrides };\n};\n\nconst generateTeam = () => ({\n  id: randUuid(),\n  name: randCompanyName(),\n  description: randParagraph(),\n  createdAt: Date.now(),\n});\n\nexport const createTeam = <T extends Partial<ReturnType<typeof generateTeam>>>(\n  overrides?: T,\n) => {\n  return { ...generateTeam(), ...overrides };\n};\n\nconst generateDiscussion = () => ({\n  id: randUuid(),\n  title: randCatchPhrase(),\n  body: randParagraph(),\n  createdAt: Date.now(),\n});\n\nexport const createDiscussion = <\n  T extends Partial<ReturnType<typeof generateDiscussion>>,\n>(\n  overrides?: T & {\n    authorId?: string;\n    teamId?: string;\n  },\n) => {\n  return { ...generateDiscussion(), ...overrides };\n};\n\nconst generateComment = () => ({\n  id: randUuid(),\n  body: randParagraph(),\n  createdAt: Date.now(),\n});\n\nexport const createComment = <\n  T extends Partial<ReturnType<typeof generateComment>>,\n>(\n  overrides?: T & {\n    authorId?: string;\n    discussionId?: string;\n  },\n) => {\n  return { ...generateComment(), ...overrides };\n};\n"
  },
  {
    "path": "apps/react-vite/src/testing/mocks/browser.ts",
    "content": "import { setupWorker } from 'msw/browser';\n\nimport { handlers } from './handlers';\n\nexport const worker = setupWorker(...handlers);\n"
  },
  {
    "path": "apps/react-vite/src/testing/mocks/db.ts",
    "content": "import { factory, primaryKey } from '@mswjs/data';\nimport { nanoid } from 'nanoid';\n\nconst models = {\n  user: {\n    id: primaryKey(nanoid),\n    firstName: String,\n    lastName: String,\n    email: String,\n    password: String,\n    teamId: String,\n    role: String,\n    bio: String,\n    createdAt: Date.now,\n  },\n  team: {\n    id: primaryKey(nanoid),\n    name: String,\n    description: String,\n    createdAt: Date.now,\n  },\n  discussion: {\n    id: primaryKey(nanoid),\n    title: String,\n    body: String,\n    authorId: String,\n    teamId: String,\n    createdAt: Date.now,\n  },\n  comment: {\n    id: primaryKey(nanoid),\n    body: String,\n    authorId: String,\n    discussionId: String,\n    createdAt: Date.now,\n  },\n};\n\nexport const db = factory(models);\n\nexport type Model = keyof typeof models;\n\nconst dbFilePath = 'mocked-db.json';\n\nexport const loadDb = async () => {\n  // If we are running in a Node.js environment\n  if (typeof window === 'undefined') {\n    const { readFile, writeFile } = await import('fs/promises');\n    try {\n      const data = await readFile(dbFilePath, 'utf8');\n      return JSON.parse(data);\n    } catch (error: any) {\n      if (error?.code === 'ENOENT') {\n        const emptyDB = {};\n        await writeFile(dbFilePath, JSON.stringify(emptyDB, null, 2));\n        return emptyDB;\n      } else {\n        console.error('Error loading mocked DB:', error);\n        return null;\n      }\n    }\n  }\n  // If we are running in a browser environment\n  return Object.assign(\n    JSON.parse(window.localStorage.getItem('msw-db') || '{}'),\n  );\n};\n\nexport const storeDb = async (data: string) => {\n  // If we are running in a Node.js environment\n  if (typeof window === 'undefined') {\n    const { writeFile } = await import('fs/promises');\n    await writeFile(dbFilePath, data);\n  } else {\n    // If we are running in a browser environment\n    window.localStorage.setItem('msw-db', data);\n  }\n};\n\nexport const persistDb = async (model: Model) => {\n  if (process.env.NODE_ENV === 'test') return;\n  const data = await loadDb();\n  data[model] = db[model].getAll();\n  await storeDb(JSON.stringify(data));\n};\n\nexport const initializeDb = async () => {\n  const database = await loadDb();\n  Object.entries(db).forEach(([key, model]) => {\n    const dataEntres = database[key];\n    if (dataEntres) {\n      dataEntres?.forEach((entry: Record<string, any>) => {\n        model.create(entry);\n      });\n    }\n  });\n};\n\nexport const resetDb = () => {\n  window.localStorage.clear();\n};\n"
  },
  {
    "path": "apps/react-vite/src/testing/mocks/handlers/auth.ts",
    "content": "import Cookies from 'js-cookie';\nimport { HttpResponse, http } from 'msw';\n\nimport { env } from '@/config/env';\n\nimport { db, persistDb } from '../db';\nimport {\n  authenticate,\n  hash,\n  requireAuth,\n  AUTH_COOKIE,\n  networkDelay,\n} from '../utils';\n\ntype RegisterBody = {\n  firstName: string;\n  lastName: string;\n  email: string;\n  password: string;\n  teamId?: string;\n  teamName?: string;\n};\n\ntype LoginBody = {\n  email: string;\n  password: string;\n};\n\nexport const authHandlers = [\n  http.post(`${env.API_URL}/auth/register`, async ({ request }) => {\n    await networkDelay();\n    try {\n      const userObject = (await request.json()) as RegisterBody;\n\n      const existingUser = db.user.findFirst({\n        where: {\n          email: {\n            equals: userObject.email,\n          },\n        },\n      });\n\n      if (existingUser) {\n        return HttpResponse.json(\n          { message: 'The user already exists' },\n          { status: 400 },\n        );\n      }\n\n      let teamId;\n      let role;\n\n      if (!userObject.teamId) {\n        const team = db.team.create({\n          name: userObject.teamName ?? `${userObject.firstName} Team`,\n        });\n        await persistDb('team');\n        teamId = team.id;\n        role = 'ADMIN';\n      } else {\n        const existingTeam = db.team.findFirst({\n          where: {\n            id: {\n              equals: userObject.teamId,\n            },\n          },\n        });\n\n        if (!existingTeam) {\n          return HttpResponse.json(\n            {\n              message: 'The team you are trying to join does not exist!',\n            },\n            { status: 400 },\n          );\n        }\n        teamId = userObject.teamId;\n        role = 'USER';\n      }\n\n      db.user.create({\n        ...userObject,\n        role,\n        password: hash(userObject.password),\n        teamId,\n      });\n\n      await persistDb('user');\n\n      const result = authenticate({\n        email: userObject.email,\n        password: userObject.password,\n      });\n\n      // todo: remove once tests in Github Actions are fixed\n      Cookies.set(AUTH_COOKIE, result.jwt, { path: '/' });\n\n      return HttpResponse.json(result, {\n        headers: {\n          // with a real API servier, the token cookie should also be Secure and HttpOnly\n          'Set-Cookie': `${AUTH_COOKIE}=${result.jwt}; Path=/;`,\n        },\n      });\n    } catch (error: any) {\n      return HttpResponse.json(\n        { message: error?.message || 'Server Error' },\n        { status: 500 },\n      );\n    }\n  }),\n\n  http.post(`${env.API_URL}/auth/login`, async ({ request }) => {\n    await networkDelay();\n\n    try {\n      const credentials = (await request.json()) as LoginBody;\n      const result = authenticate(credentials);\n\n      // todo: remove once tests in Github Actions are fixed\n      Cookies.set(AUTH_COOKIE, result.jwt, { path: '/' });\n\n      return HttpResponse.json(result, {\n        headers: {\n          // with a real API servier, the token cookie should also be Secure and HttpOnly\n          'Set-Cookie': `${AUTH_COOKIE}=${result.jwt}; Path=/;`,\n        },\n      });\n    } catch (error: any) {\n      return HttpResponse.json(\n        { message: error?.message || 'Server Error' },\n        { status: 500 },\n      );\n    }\n  }),\n\n  http.post(`${env.API_URL}/auth/logout`, async () => {\n    await networkDelay();\n\n    // todo: remove once tests in Github Actions are fixed\n    Cookies.remove(AUTH_COOKIE);\n\n    return HttpResponse.json(\n      { message: 'Logged out' },\n      {\n        headers: {\n          'Set-Cookie': `${AUTH_COOKIE}=; Path=/;`,\n        },\n      },\n    );\n  }),\n\n  http.get(`${env.API_URL}/auth/me`, async ({ cookies }) => {\n    await networkDelay();\n\n    try {\n      const { user } = requireAuth(cookies);\n      return HttpResponse.json({ data: user });\n    } catch (error: any) {\n      return HttpResponse.json(\n        { message: error?.message || 'Server Error' },\n        { status: 500 },\n      );\n    }\n  }),\n];\n"
  },
  {
    "path": "apps/react-vite/src/testing/mocks/handlers/comments.ts",
    "content": "import { HttpResponse, http } from 'msw';\n\nimport { env } from '@/config/env';\n\nimport { db, persistDb } from '../db';\nimport { networkDelay, requireAuth, sanitizeUser } from '../utils';\n\ntype CreateCommentBody = {\n  body: string;\n  discussionId: string;\n};\n\nexport const commentsHandlers = [\n  http.get(`${env.API_URL}/comments`, async ({ request, cookies }) => {\n    await networkDelay();\n\n    try {\n      const { error } = requireAuth(cookies);\n      if (error) {\n        return HttpResponse.json({ message: error }, { status: 401 });\n      }\n      const url = new URL(request.url);\n      const discussionId = url.searchParams.get('discussionId') || '';\n      const page = Number(url.searchParams.get('page') || 1);\n\n      const total = db.comment.count({\n        where: {\n          discussionId: {\n            equals: discussionId,\n          },\n        },\n      });\n\n      const totalPages = Math.ceil(total / 10);\n\n      const comments = db.comment\n        .findMany({\n          where: {\n            discussionId: {\n              equals: discussionId,\n            },\n          },\n          take: 10,\n          skip: 10 * (page - 1),\n        })\n        .map(({ authorId, ...comment }) => {\n          const author = db.user.findFirst({\n            where: {\n              id: {\n                equals: authorId,\n              },\n            },\n          });\n          return {\n            ...comment,\n            author: author ? sanitizeUser(author) : {},\n          };\n        });\n      return HttpResponse.json({\n        data: comments,\n        meta: {\n          page,\n          total,\n          totalPages,\n        },\n      });\n    } catch (error: any) {\n      return HttpResponse.json(\n        { message: error?.message || 'Server Error' },\n        { status: 500 },\n      );\n    }\n  }),\n\n  http.post(`${env.API_URL}/comments`, async ({ request, cookies }) => {\n    await networkDelay();\n\n    try {\n      const { user, error } = requireAuth(cookies);\n      if (error) {\n        return HttpResponse.json({ message: error }, { status: 401 });\n      }\n      const data = (await request.json()) as CreateCommentBody;\n      const result = db.comment.create({\n        authorId: user?.id,\n        ...data,\n      });\n      await persistDb('comment');\n      return HttpResponse.json(result);\n    } catch (error: any) {\n      return HttpResponse.json(\n        { message: error?.message || 'Server Error' },\n        { status: 500 },\n      );\n    }\n  }),\n\n  http.delete(\n    `${env.API_URL}/comments/:commentId`,\n    async ({ params, cookies }) => {\n      await networkDelay();\n\n      try {\n        const { user, error } = requireAuth(cookies);\n        if (error) {\n          return HttpResponse.json({ message: error }, { status: 401 });\n        }\n        const commentId = params.commentId as string;\n        const result = db.comment.delete({\n          where: {\n            id: {\n              equals: commentId,\n            },\n            ...(user?.role === 'USER' && {\n              authorId: {\n                equals: user.id,\n              },\n            }),\n          },\n        });\n        await persistDb('comment');\n        return HttpResponse.json(result);\n      } catch (error: any) {\n        return HttpResponse.json(\n          { message: error?.message || 'Server Error' },\n          { status: 500 },\n        );\n      }\n    },\n  ),\n];\n"
  },
  {
    "path": "apps/react-vite/src/testing/mocks/handlers/discussions.ts",
    "content": "import { HttpResponse, http } from 'msw';\n\nimport { env } from '@/config/env';\n\nimport { db, persistDb } from '../db';\nimport {\n  requireAuth,\n  requireAdmin,\n  sanitizeUser,\n  networkDelay,\n} from '../utils';\n\ntype DiscussionBody = {\n  title: string;\n  body: string;\n};\n\nexport const discussionsHandlers = [\n  http.get(`${env.API_URL}/discussions`, async ({ cookies, request }) => {\n    await networkDelay();\n\n    try {\n      const { user, error } = requireAuth(cookies);\n      if (error) {\n        return HttpResponse.json({ message: error }, { status: 401 });\n      }\n\n      const url = new URL(request.url);\n\n      const page = Number(url.searchParams.get('page') || 1);\n\n      const total = db.discussion.count({\n        where: {\n          teamId: {\n            equals: user?.teamId,\n          },\n        },\n      });\n\n      const totalPages = Math.ceil(total / 10);\n\n      const result = db.discussion\n        .findMany({\n          where: {\n            teamId: {\n              equals: user?.teamId,\n            },\n          },\n          take: 10,\n          skip: 10 * (page - 1),\n        })\n        .map(({ authorId, ...discussion }) => {\n          const author = db.user.findFirst({\n            where: {\n              id: {\n                equals: authorId,\n              },\n            },\n          });\n          return {\n            ...discussion,\n            author: author ? sanitizeUser(author) : {},\n          };\n        });\n      return HttpResponse.json({\n        data: result,\n        meta: {\n          page,\n          total,\n          totalPages,\n        },\n      });\n    } catch (error: any) {\n      return HttpResponse.json(\n        { message: error?.message || 'Server Error' },\n        { status: 500 },\n      );\n    }\n  }),\n\n  http.get(\n    `${env.API_URL}/discussions/:discussionId`,\n    async ({ params, cookies }) => {\n      await networkDelay();\n\n      try {\n        const { user, error } = requireAuth(cookies);\n        if (error) {\n          return HttpResponse.json({ message: error }, { status: 401 });\n        }\n        const discussionId = params.discussionId as string;\n        const discussion = db.discussion.findFirst({\n          where: {\n            id: {\n              equals: discussionId,\n            },\n            teamId: {\n              equals: user?.teamId,\n            },\n          },\n        });\n\n        if (!discussion) {\n          return HttpResponse.json(\n            { message: 'Discussion not found' },\n            { status: 404 },\n          );\n        }\n\n        const author = db.user.findFirst({\n          where: {\n            id: {\n              equals: discussion.authorId,\n            },\n          },\n        });\n\n        const result = {\n          ...discussion,\n          author: author ? sanitizeUser(author) : {},\n        };\n\n        return HttpResponse.json({ data: result });\n      } catch (error: any) {\n        return HttpResponse.json(\n          { message: error?.message || 'Server Error' },\n          { status: 500 },\n        );\n      }\n    },\n  ),\n\n  http.post(`${env.API_URL}/discussions`, async ({ request, cookies }) => {\n    await networkDelay();\n\n    try {\n      const { user, error } = requireAuth(cookies);\n      if (error) {\n        return HttpResponse.json({ message: error }, { status: 401 });\n      }\n      const data = (await request.json()) as DiscussionBody;\n      requireAdmin(user);\n      const result = db.discussion.create({\n        teamId: user?.teamId,\n        authorId: user?.id,\n        ...data,\n      });\n      await persistDb('discussion');\n      return HttpResponse.json(result);\n    } catch (error: any) {\n      return HttpResponse.json(\n        { message: error?.message || 'Server Error' },\n        { status: 500 },\n      );\n    }\n  }),\n\n  http.patch(\n    `${env.API_URL}/discussions/:discussionId`,\n    async ({ request, params, cookies }) => {\n      await networkDelay();\n\n      try {\n        const { user, error } = requireAuth(cookies);\n        if (error) {\n          return HttpResponse.json({ message: error }, { status: 401 });\n        }\n        const data = (await request.json()) as DiscussionBody;\n        const discussionId = params.discussionId as string;\n        requireAdmin(user);\n        const result = db.discussion.update({\n          where: {\n            teamId: {\n              equals: user?.teamId,\n            },\n            id: {\n              equals: discussionId,\n            },\n          },\n          data,\n        });\n        await persistDb('discussion');\n        return HttpResponse.json(result);\n      } catch (error: any) {\n        return HttpResponse.json(\n          { message: error?.message || 'Server Error' },\n          { status: 500 },\n        );\n      }\n    },\n  ),\n\n  http.delete(\n    `${env.API_URL}/discussions/:discussionId`,\n    async ({ cookies, params }) => {\n      await networkDelay();\n\n      try {\n        const { user, error } = requireAuth(cookies);\n        if (error) {\n          return HttpResponse.json({ message: error }, { status: 401 });\n        }\n        const discussionId = params.discussionId as string;\n        requireAdmin(user);\n        const result = db.discussion.delete({\n          where: {\n            id: {\n              equals: discussionId,\n            },\n          },\n        });\n        await persistDb('discussion');\n        return HttpResponse.json(result);\n      } catch (error: any) {\n        return HttpResponse.json(\n          { message: error?.message || 'Server Error' },\n          { status: 500 },\n        );\n      }\n    },\n  ),\n];\n"
  },
  {
    "path": "apps/react-vite/src/testing/mocks/handlers/index.ts",
    "content": "import { HttpResponse, http } from 'msw';\n\nimport { env } from '@/config/env';\n\nimport { networkDelay } from '../utils';\n\nimport { authHandlers } from './auth';\nimport { commentsHandlers } from './comments';\nimport { discussionsHandlers } from './discussions';\nimport { teamsHandlers } from './teams';\nimport { usersHandlers } from './users';\n\nexport const handlers = [\n  ...authHandlers,\n  ...commentsHandlers,\n  ...discussionsHandlers,\n  ...teamsHandlers,\n  ...usersHandlers,\n  http.get(`${env.API_URL}/healthcheck`, async () => {\n    await networkDelay();\n    return HttpResponse.json({ ok: true });\n  }),\n];\n"
  },
  {
    "path": "apps/react-vite/src/testing/mocks/handlers/teams.ts",
    "content": "import { HttpResponse, http } from 'msw';\n\nimport { env } from '@/config/env';\n\nimport { db } from '../db';\nimport { networkDelay } from '../utils';\n\nexport const teamsHandlers = [\n  http.get(`${env.API_URL}/teams`, async () => {\n    await networkDelay();\n\n    try {\n      const result = db.team.getAll();\n      return HttpResponse.json({ data: result });\n    } catch (error: any) {\n      return HttpResponse.json(\n        { message: error?.message || 'Server Error' },\n        { status: 500 },\n      );\n    }\n  }),\n];\n"
  },
  {
    "path": "apps/react-vite/src/testing/mocks/handlers/users.ts",
    "content": "import { HttpResponse, http } from 'msw';\n\nimport { env } from '@/config/env';\n\nimport { db, persistDb } from '../db';\nimport {\n  requireAuth,\n  requireAdmin,\n  sanitizeUser,\n  networkDelay,\n} from '../utils';\n\ntype ProfileBody = {\n  email: string;\n  firstName: string;\n  lastName: string;\n  bio: string;\n};\n\nexport const usersHandlers = [\n  http.get(`${env.API_URL}/users`, async ({ cookies }) => {\n    await networkDelay();\n\n    try {\n      const { user, error } = requireAuth(cookies);\n      if (error) {\n        return HttpResponse.json({ message: error }, { status: 401 });\n      }\n      const result = db.user\n        .findMany({\n          where: {\n            teamId: {\n              equals: user?.teamId,\n            },\n          },\n        })\n        .map(sanitizeUser);\n\n      return HttpResponse.json({ data: result });\n    } catch (error: any) {\n      return HttpResponse.json(\n        { message: error?.message || 'Server Error' },\n        { status: 500 },\n      );\n    }\n  }),\n\n  http.patch(`${env.API_URL}/users/profile`, async ({ request, cookies }) => {\n    await networkDelay();\n\n    try {\n      const { user, error } = requireAuth(cookies);\n      if (error) {\n        return HttpResponse.json({ message: error }, { status: 401 });\n      }\n      const data = (await request.json()) as ProfileBody;\n      const result = db.user.update({\n        where: {\n          id: {\n            equals: user?.id,\n          },\n        },\n        data,\n      });\n      await persistDb('user');\n      return HttpResponse.json(result);\n    } catch (error: any) {\n      return HttpResponse.json(\n        { message: error?.message || 'Server Error' },\n        { status: 500 },\n      );\n    }\n  }),\n\n  http.delete(`${env.API_URL}/users/:userId`, async ({ cookies, params }) => {\n    await networkDelay();\n\n    try {\n      const { user, error } = requireAuth(cookies);\n      if (error) {\n        return HttpResponse.json({ message: error }, { status: 401 });\n      }\n      const userId = params.userId as string;\n      requireAdmin(user);\n      const result = db.user.delete({\n        where: {\n          id: {\n            equals: userId,\n          },\n          teamId: {\n            equals: user?.teamId,\n          },\n        },\n      });\n      await persistDb('user');\n      return HttpResponse.json(result);\n    } catch (error: any) {\n      return HttpResponse.json(\n        { message: error?.message || 'Server Error' },\n        { status: 500 },\n      );\n    }\n  }),\n];\n"
  },
  {
    "path": "apps/react-vite/src/testing/mocks/index.ts",
    "content": "import { env } from '@/config/env';\n\nexport const enableMocking = async () => {\n  if (env.ENABLE_API_MOCKING) {\n    const { worker } = await import('./browser');\n    const { initializeDb } = await import('./db');\n    await initializeDb();\n    return worker.start();\n  }\n};\n"
  },
  {
    "path": "apps/react-vite/src/testing/mocks/server.ts",
    "content": "import { setupServer } from 'msw/node';\n\nimport { handlers } from './handlers';\n\nexport const server = setupServer(...handlers);\n"
  },
  {
    "path": "apps/react-vite/src/testing/mocks/utils.ts",
    "content": "import Cookies from 'js-cookie';\nimport { delay } from 'msw';\n\nimport { db } from './db';\n\nexport const encode = (obj: any) => {\n  const btoa =\n    typeof window === 'undefined'\n      ? (str: string) => Buffer.from(str, 'binary').toString('base64')\n      : window.btoa;\n  return btoa(JSON.stringify(obj));\n};\n\nexport const decode = (str: string) => {\n  const atob =\n    typeof window === 'undefined'\n      ? (str: string) => Buffer.from(str, 'base64').toString('binary')\n      : window.atob;\n  return JSON.parse(atob(str));\n};\n\nexport const hash = (str: string) => {\n  let hash = 5381,\n    i = str.length;\n\n  while (i) {\n    hash = (hash * 33) ^ str.charCodeAt(--i);\n  }\n  return String(hash >>> 0);\n};\n\nexport const networkDelay = () => {\n  const delayTime = import.meta.env.TEST\n    ? 200\n    : Math.floor(Math.random() * 700) + 300;\n  return delay(delayTime);\n};\n\nconst omit = <T extends object>(obj: T, keys: string[]): T => {\n  const result = {} as T;\n  for (const key in obj) {\n    if (!keys.includes(key)) {\n      result[key] = obj[key];\n    }\n  }\n\n  return result;\n};\n\nexport const sanitizeUser = <O extends object>(user: O) =>\n  omit<O>(user, ['password', 'iat']);\n\nexport function authenticate({\n  email,\n  password,\n}: {\n  email: string;\n  password: string;\n}) {\n  const user = db.user.findFirst({\n    where: {\n      email: {\n        equals: email,\n      },\n    },\n  });\n\n  if (user?.password === hash(password)) {\n    const sanitizedUser = sanitizeUser(user);\n    const encodedToken = encode(sanitizedUser);\n    return { user: sanitizedUser, jwt: encodedToken };\n  }\n\n  const error = new Error('Invalid username or password');\n  throw error;\n}\n\nexport const AUTH_COOKIE = `bulletproof_react_app_token`;\n\nexport function requireAuth(cookies: Record<string, string>) {\n  try {\n    const encodedToken = cookies[AUTH_COOKIE] || Cookies.get(AUTH_COOKIE);\n    if (!encodedToken) {\n      return { error: 'Unauthorized', user: null };\n    }\n    const decodedToken = decode(encodedToken) as { id: string };\n\n    const user = db.user.findFirst({\n      where: {\n        id: {\n          equals: decodedToken.id,\n        },\n      },\n    });\n\n    if (!user) {\n      return { error: 'Unauthorized', user: null };\n    }\n\n    return { user: sanitizeUser(user) };\n  } catch (err: any) {\n    return { error: 'Unauthorized', user: null };\n  }\n}\n\nexport function requireAdmin(user: any) {\n  if (user.role !== 'ADMIN') {\n    throw Error('Unauthorized');\n  }\n}\n"
  },
  {
    "path": "apps/react-vite/src/testing/setup-tests.ts",
    "content": "import '@testing-library/jest-dom/vitest';\n\nimport { initializeDb, resetDb } from '@/testing/mocks/db';\nimport { server } from '@/testing/mocks/server';\n\nvi.mock('zustand');\n\nbeforeAll(() => server.listen({ onUnhandledRequest: 'error' }));\nafterAll(() => server.close());\nbeforeEach(() => {\n  const ResizeObserverMock = vi.fn(() => ({\n    observe: vi.fn(),\n    unobserve: vi.fn(),\n    disconnect: vi.fn(),\n  }));\n\n  vi.stubGlobal('ResizeObserver', ResizeObserverMock);\n\n  window.btoa = (str: string) => Buffer.from(str, 'binary').toString('base64');\n  window.atob = (str: string) => Buffer.from(str, 'base64').toString('binary');\n\n  initializeDb();\n});\nafterEach(() => {\n  server.resetHandlers();\n  resetDb();\n});\n"
  },
  {
    "path": "apps/react-vite/src/testing/test-utils.tsx",
    "content": "import {\n  render as rtlRender,\n  screen,\n  waitForElementToBeRemoved,\n} from '@testing-library/react';\nimport userEvent from '@testing-library/user-event';\nimport Cookies from 'js-cookie';\nimport { RouterProvider, createMemoryRouter } from 'react-router';\n\nimport { AppProvider } from '@/app/provider';\n\nimport {\n  createDiscussion as generateDiscussion,\n  createUser as generateUser,\n} from './data-generators';\nimport { db } from './mocks/db';\nimport { AUTH_COOKIE, authenticate, hash } from './mocks/utils';\n\nexport const createUser = async (userProperties?: any) => {\n  const user = generateUser(userProperties) as any;\n  await db.user.create({ ...user, password: hash(user.password) });\n  return user;\n};\n\nexport const createDiscussion = async (discussionProperties?: any) => {\n  const discussion = generateDiscussion(discussionProperties);\n  const res = await db.discussion.create(discussion);\n  return res;\n};\n\nexport const loginAsUser = async (user: any) => {\n  const authUser = await authenticate(user);\n  Cookies.set(AUTH_COOKIE, authUser.jwt);\n  return authUser;\n};\n\nexport const waitForLoadingToFinish = () =>\n  waitForElementToBeRemoved(\n    () => [\n      ...screen.queryAllByTestId(/loading/i),\n      ...screen.queryAllByText(/loading/i),\n    ],\n    { timeout: 4000 },\n  );\n\nconst initializeUser = async (user: any) => {\n  if (typeof user === 'undefined') {\n    const newUser = await createUser();\n    return loginAsUser(newUser);\n  } else if (user) {\n    return loginAsUser(user);\n  } else {\n    return null;\n  }\n};\n\nexport const renderApp = async (\n  ui: any,\n  { user, url = '/', path = '/', ...renderOptions }: Record<string, any> = {},\n) => {\n  // if you want to render the app unauthenticated then pass \"null\" as the user\n  const initializedUser = await initializeUser(user);\n\n  const router = createMemoryRouter(\n    [\n      {\n        path: path,\n        element: ui,\n      },\n    ],\n    {\n      initialEntries: url ? ['/', url] : ['/'],\n      initialIndex: url ? 1 : 0,\n    },\n  );\n\n  const returnValue = {\n    ...rtlRender(ui, {\n      wrapper: () => {\n        return (\n          <AppProvider>\n            <RouterProvider router={router} />\n          </AppProvider>\n        );\n      },\n      ...renderOptions,\n    }),\n    user: initializedUser,\n  };\n\n  await waitForLoadingToFinish();\n\n  return returnValue;\n};\n\nexport * from '@testing-library/react';\nexport { userEvent, rtlRender };\n"
  },
  {
    "path": "apps/react-vite/src/types/api.ts",
    "content": "// let's imagine this file is autogenerated from the backend\n// ideally, we want to keep these api related types in sync\n// with the backend instead of manually writing them out\n\nexport type BaseEntity = {\n  id: string;\n  createdAt: number;\n};\n\nexport type Entity<T> = {\n  [K in keyof T]: T[K];\n} & BaseEntity;\n\nexport type Meta = {\n  page: number;\n  total: number;\n  totalPages: number;\n};\n\nexport type User = Entity<{\n  firstName: string;\n  lastName: string;\n  email: string;\n  role: 'ADMIN' | 'USER';\n  teamId: string;\n  bio: string;\n}>;\n\nexport type AuthResponse = {\n  jwt: string;\n  user: User;\n};\n\nexport type Team = Entity<{\n  name: string;\n  description: string;\n}>;\n\nexport type Discussion = Entity<{\n  title: string;\n  body: string;\n  teamId: string;\n  author: User;\n}>;\n\nexport type Comment = Entity<{\n  body: string;\n  discussionId: string;\n  author: User;\n}>;\n"
  },
  {
    "path": "apps/react-vite/src/utils/cn.ts",
    "content": "import { type ClassValue, clsx } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"
  },
  {
    "path": "apps/react-vite/src/utils/format.ts",
    "content": "import { default as dayjs } from 'dayjs';\n\nexport const formatDate = (date: number) =>\n  dayjs(date).format('MMMM D, YYYY h:mm A');\n"
  },
  {
    "path": "apps/react-vite/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "apps/react-vite/tailwind.config.cjs",
    "content": "/** @type {import('tailwindcss').Config} */\n\nconst defaultTheme = require('tailwindcss/defaultTheme');\n\nmodule.exports = {\n  content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],\n  theme: {\n    container: {\n      center: true,\n      padding: '2rem',\n      screens: {\n        '2xl': '1400px',\n      },\n    },\n    extend: {\n      fontFamily: {\n        sans: ['Inter var', ...defaultTheme.fontFamily.sans],\n      },\n      colors: {\n        border: 'hsl(var(--border))',\n        input: 'hsl(var(--input))',\n        ring: 'hsl(var(--ring))',\n        background: 'hsl(var(--background))',\n        foreground: 'hsl(var(--foreground))',\n        primary: {\n          DEFAULT: 'hsl(var(--primary))',\n          foreground: 'hsl(var(--primary-foreground))',\n        },\n        secondary: {\n          DEFAULT: 'hsl(var(--secondary))',\n          foreground: 'hsl(var(--secondary-foreground))',\n        },\n        destructive: {\n          DEFAULT: 'hsl(var(--destructive))',\n          foreground: 'hsl(var(--destructive-foreground))',\n        },\n        muted: {\n          DEFAULT: 'hsl(var(--muted))',\n          foreground: 'hsl(var(--muted-foreground))',\n        },\n        accent: {\n          DEFAULT: 'hsl(var(--accent))',\n          foreground: 'hsl(var(--accent-foreground))',\n        },\n        popover: {\n          DEFAULT: 'hsl(var(--popover))',\n          foreground: 'hsl(var(--popover-foreground))',\n        },\n        card: {\n          DEFAULT: 'hsl(var(--card))',\n          foreground: 'hsl(var(--card-foreground))',\n        },\n      },\n      borderRadius: {\n        lg: 'var(--radius)',\n        md: 'calc(var(--radius) - 2px)',\n        sm: 'calc(var(--radius) - 4px)',\n      },\n      keyframes: {\n        'accordion-down': {\n          from: { height: '0' },\n          to: { height: 'var(--radix-accordion-content-height)' },\n        },\n        'accordion-up': {\n          from: { height: 'var(--radix-accordion-content-height)' },\n          to: { height: '0' },\n        },\n      },\n      animation: {\n        'accordion-down': 'accordion-down 0.2s ease-out',\n        'accordion-up': 'accordion-up 0.2s ease-out',\n      },\n    },\n  },\n  plugins: [require('tailwindcss-animate'), require('@tailwindcss/typography')],\n};\n"
  },
  {
    "path": "apps/react-vite/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n    \"types\": [\"vite/client\", \"vitest/globals\"],\n    \"isolatedModules\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"./src/*\"]\n    }\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "apps/react-vite/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "apps/react-vite/vite.config.ts",
    "content": "/// <reference types=\"vitest\" />\n/// <reference types=\"vite/client\" />\n\nimport react from '@vitejs/plugin-react';\nimport { defineConfig } from 'vite';\nimport viteTsconfigPaths from 'vite-tsconfig-paths';\n\nexport default defineConfig({\n  base: './',\n  plugins: [react(), viteTsconfigPaths()],\n  server: {\n    port: 3000,\n  },\n  preview: {\n    port: 3000,\n  },\n  test: {\n    globals: true,\n    environment: 'jsdom',\n    setupFiles: './src/testing/setup-tests.ts',\n    exclude: ['**/node_modules/**', '**/e2e/**'],\n    coverage: {\n      include: ['src/**'],\n    },\n  },\n  optimizeDeps: { exclude: ['fsevents'] },\n  build: {\n    rollupOptions: {\n      external: ['fs/promises'],\n      output: {\n        experimentalMinChunkSize: 3500,\n      },\n    },\n  },\n});\n"
  },
  {
    "path": "docs/additional-resources.md",
    "content": "# 📚 Additional Resources\n\n## React\n\n- [Official Documentation](https://react.dev/)\n- [Tao Of React](https://alexkondov.com/tao-of-react/)\n- [React Handbook](https://reacthandbook.dev/)\n- [React Philosophies](https://github.com/mithi/react-philosophies)\n- [React Patterns](https://reactpatterns.com/)\n- [React Typescript Cheatsheet](https://react-typescript-cheatsheet.netlify.app/)\n\n## JavaScript\n\n- [You Dont Know JS](https://github.com/getify/You-Dont-Know-JS)\n- [JavaScript Info](https://javascript.info/)\n- [33 Concepts Every JavaScript Developer Should Know](https://github.com/leonardomso/33-js-concepts#8-iife-modules-and-namespaces)\n- [JavaScript to Know for React](https://kentcdodds.com/blog/javascript-to-know-for-react)\n\n## Best Practices\n\n- [patterns.dev](https://www.patterns.dev/)\n- [Naming Cheatsheet](https://github.com/kettanaito/naming-cheatsheet)\n- [Clean Code Javascript](https://github.com/ryanmcdermott/clean-code-javascript)\n"
  },
  {
    "path": "docs/api-layer.md",
    "content": "# 📡 API Layer\n\n### Use a Single Instance of the API Client\n\nWhen your application interacts with either RESTful or GraphQL APIs, it is beneficial to use a single instance of the API client that has been pre-configured and can be reused throughout the application. For example, you can create a single API client instance using the native fetch API or libraries such as [axios](https://github.com/axios/axios), [graphql-request](https://github.com/prisma-labs/graphql-request), or [apollo-client](https://www.apollographql.com/docs/react/) with predefined configuration settings.\n\n[API Client Example Code](../apps/react-vite/src/lib/api-client.ts)\n\n### Define and Export Request Declarations\n\nRather than declaring API requests on the fly, it is recommended to define and export them separately.\n\nDeclaring API requests in a structured manner can help maintain a clean and organized codebase as everything is colocated.\nEvery API request declaration should consist of:\n\n- Types and validation schemas for the request and response data\n- A fetcher function that calls an endpoint, using the API client instance\n- A hook that consumes the fetcher function that is built on top of libraries such as [react-query](https://tanstack.com/query), [swr](https://swr.vercel.app/), [apollo-client](https://www.apollographql.com/docs/react/), [urql](https://formidable.com/open-source/urql/), etc. to manage the data fetching and caching logic.\n\nThis approach simplifies the tracking of defined endpoints available in the application. Additionally, typing the responses and inferring them further down the application enhances application type safety.\n\n[API Request Declarations - Query - Example Code](../apps/react-vite/src/features/discussions/api/get-discussions.ts)\n[API Request Declarations - Mutation - Example Code](../apps/react-vite/src/features/discussions/api/create-discussion.ts)\n"
  },
  {
    "path": "docs/application-overview.md",
    "content": "# 💻 Application Overview\n\nThe application is relatively simple. Users can create teams where other users can join, and they start discussions on different topics between each other.\n\nA team is created during the registration if the user didn't choose to join an existing team and the user becomes the admin of it.\n\n[Demo](https://bulletproof-react-app.netlify.app)\n\n## Data model\n\nThe application contains the following models:\n\n- User - can have one of these roles:\n\n  - `ADMIN` can:\n    - create/edit/delete discussions\n    - create/delete all comments\n    - delete users\n    - edit own profile\n  - `USER` - can:\n    - edit own profile\n    - create/delete own comments\n\n- Team: represents a team that has 1 admin and many users that can participate in discussions between each other.\n\n- Discussion: represents discussions created by team members.\n\n- Comment: represents all the messages in a discussion.\n\n## Get Started\n\nTo get started, check the README.md file in the application you want to run.\n\n- [React Vite](../apps/react-vite/README.md)\n- [Next.js App Router](../apps/nextjs-app/README.md)\n- [Next.js Pages](../apps/nextjs-pages/README.md)\n"
  },
  {
    "path": "docs/components-and-styling.md",
    "content": "# 🧱 Components And Styling\n\n## Components Best Practices\n\n#### Colocate things as close as possible to where it's being used\n\nKeep components, functions, styles, state, etc. as close as possible to where they are being used. This will not only make your codebase more readable and easier to understand but it will also improve your application performance since it will reduce redundant re-renders on state updates.\n\n#### Avoid large components with nested rendering functions\n\nDo not add multiple rendering functions inside your application, this gets out of control pretty quickly. What you should do instead is if there is a piece of UI that can be considered as a unit, is to extract it in a separate component.\n\n```javascript\n// this is very difficult to maintain as soon as the component starts growing\nfunction Component() {\n  function renderItems() {\n    return <ul>...</ul>;\n  }\n  return <div>{renderItems()}</div>;\n}\n\n// extract it in a separate component\nfunction Items() {\n  return <ul>...</ul>;\n}\n\nfunction Component() {\n  return (\n    <div>\n      <Items />\n    </div>\n  );\n}\n```\n\n#### Stay consistent\n\nKeep your code style consistent. For example, if you name your components using pascal case, do it everywhere. Most of code consistency is achieved by using linters and code formatters, so make sure you have them set up in your project.\n\n#### Limit the number of props a component is accepting as input\n\nIf your component is accepting too many props you might consider splitting it into multiple components or use the composition technique via children or slots.\n\n[Composition Example Code](../apps/react-vite/src/components/ui/dialog/confirmation-dialog/confirmation-dialog.tsx)\n\n#### Abstract shared components into a component library\n\nFor larger projects, it is a good idea to build abstractions around all the shared components. It makes the application more consistent and easier to maintain. Identify repetitions before creating the components to avoid wrong abstractions.\n\n[Component Library Example Code](../apps/react-vite/src/components/ui/button/button.tsx)\n\nIt is a good idea to wrap 3rd party components as well in order to adapt them to the application's needs. It might be easier to make the underlying changes in the future without affecting the application's functionality.\n\n[3rd Party Component Example Code](../apps/react-vite/src/components/ui/link/link.tsx)\n\n## Component libraries\n\nEvery project requires some UI components such as modals, tabs, sidebars, menus, etc. Instead of building those from scratch, you might want to use some of the existing, battle-tested component libraries.\n\n#### Fully featured component libraries:\n\nThese component libraries come with their components fully styled.\n\n- [Chakra UI](https://chakra-ui.com/) - great library with great developer experience, allows very fast prototyping with decent design defaults. Plenty of components that are very customizable and flexible with accessibility already configured out of the box.\n\n- [AntD](https://ant.design/) - another great component library that has a lot of different components. Best suitable for creating admin dashboards. However, it might be a bit difficult to change the styles in order to adapt them to a custom design.\n\n- [MUI](https://mui.com/material-ui/) - the most popular component library for React. Has a lot of different components. Can be used as a styled solution by implementing Material Design or as unstyled headless component library.\n\n- [Mantine](https://mantine.dev/) - a modern react component library with a lot of components and hooks. It is very customizable and has a lot of features out of the box.\n\n#### Headless component libraries:\n\nThese component libraries come with their components unstyled. If you have a specific design system to implement, it might be easier and better solution to go with headless components that come unstyled than to adapt a fully featured component library such as Material UI to your needs. Some good options are:\n\n- [Radix UI](https://www.radix-ui.com/)\n- [Headless UI](https://headlessui.dev/)\n- [react-aria](https://react-spectrum.adobe.com/react-aria/)\n- [Ark UI](https://ark-ui.com/)\n- [Reakit](https://reakit.io/)\n\n## Styling Solutions\n\nThere are multiple ways to style a react application. Some good options are:\n\n- [tailwind](https://tailwindcss.com/)\n- [vanilla-extract](https://github.com/seek-oss/vanilla-extract)\n- [Panda CSS](https://panda-css.com/)\n- [CSS modules](https://github.com/css-modules/css-modules)\n- [styled-components](https://styled-components.com/)\n- [emotion](https://emotion.sh/docs/introduction)\n\nNOTE: Keep React Server Components in mind as they require zero runtime styling solution.\n\nWith the rise of headless component libraries, there is another tier of component libraries where predefined components are provided with styling solutions included, but instead of being installed as a package, they are provided as code which can be customized and styled as needed.\n\n- [ShadCN UI](https://ui.shadcn.com/)\n- [Park UI](https://park-ui.com/)\n\n## Storybook\n\n[Storybook](https://storybook.js.org/) is a great tool for developing and testing components in isolation. Think of it as a catalogue of all the components your application is using. Very useful for developing and discoverability of components.\n\n[Storybook Story Example Code](../apps/react-vite/src/components/ui/button/button.stories.tsx)\n"
  },
  {
    "path": "docs/deployment.md",
    "content": "# 🌐 Deployment\n\nDeploy and serve your applications and assets over a CDN for best delivery and performance. Good options for that are:\n\n- [Vercel](https://vercel.com/)\n- [Netlify](https://www.netlify.com/)\n- [AWS](https://aws.amazon.com/cloudfront/)\n- [CloudFlare](https://www.cloudflare.com/en-gb/cdn/)\n"
  },
  {
    "path": "docs/error-handling.md",
    "content": "# ⚠️ Error Handling\n\n### API Errors\n\nImplement an interceptor to manage errors effectively. This interceptor can be utilized to trigger notification toasts informing users of errors, log out unauthorized users, or send requests to refresh tokens to maintain secure and seamless application operation.\n\n[API Errors Notification Example Code](../apps/react-vite/src/lib/api-client.ts)\n\n### In App Errors\n\nUtilize error boundaries in React to handle errors within specific parts of your application. Instead of having only one error boundary for the entire app, consider placing multiple error boundaries in different areas. This way, if an error occurs, it can be contained and managed locally without disrupting the entire application's functionality, ensuring a smoother user experience.\n\n[Error Boundary Example Code](../apps/react-vite/src/app/routes/app/discussions/discussion.tsx)\n\n### Error Tracking\n\nYou should track any errors that occur in production. Although it's possible to implement your own solution, it is a better idea to use tools like [Sentry](https://sentry.io/). It will report any issue that breaks the app. You will also be able to see on which platform, browser, etc. did it occur. Make sure to upload source maps to sentry to see where in your source code did the error happen.\n"
  },
  {
    "path": "docs/performance.md",
    "content": "# 🚄 Performance\n\n### Code Splitting\n\nCode splitting involves splitting production JavaScript into smaller files to optimize application loading times. This technique enables the application to be downloaded in parts, fetching only the necessary code when required.\n\nIdeally, code splitting should be implemented at the routes level, ensuring that only essential code is loaded initially, with additional parts fetched lazily as needed. It's important to avoid excessive code splitting, as this can lead to a performance decline due to the increased number of requests required to fetch all the code chunks. Strategic code splitting, focusing on critical parts of the application, helps balance performance optimization with efficient resource loading.\n\n[Code Splitting Example Code](../apps/react-vite/src/app/router.tsx)\n\n### Component and state optimizations\n\n- Do not put everything in a single state. That might trigger unnecessary re-renders. Instead split the global state into multiple states according to where they are being used.\n\n- Keep the state as close as possible to where it is being used. This will prevent re-rendering components that do not depend on the updated state.\n\n- If you have a piece of state that is initialized by an expensive computation, use the state initializer function instead of executing it directly because the expensive function will be run only once as it is supposed to. e.g:\n\n```javascript\n// instead of this which would be executed on every re-render:\nconst [state, setState] = React.useState(myExpensiveFn());\n\n// prefer this which is executed only once:\nconst [state, setState] = React.useState(() => myExpensiveFn());\n```\n\n- If you develop an application that requires a state to track many elements at once, you might consider state management libraries with atomic updates such as [jotai](https://jotai.pmnd.rs/).\n\n- Use React Context wisely. React Context is good for low-velocity data like themes, user data, small local state etc. While dealing with medium-velocity/high-velocity data, you may consider using the [use-context-selector](https://github.com/dai-shi/use-context-selector) library that supports selectors (selectors are already built-in in most popular state management libraries like [zustand](https://docs.pmnd.rs/zustand/getting-started/introduction) or [jotai](https://jotai.org/)). Important to remember, context is often used as the \"golden tool\" for props drilling, whereas in many scenarios you may satisfy your needs by [lifting the state up](https://react.dev/learn/sharing-state-between-components#lifting-state-up-by-example) or [a proper composition of components](https://react.dev/learn/passing-data-deeply-with-context#before-you-use-context). Do not rush with context and global state.\n\n- If your application is expected to have frequent updates that might affect performance, consider switching from runtime styling solutions such as [emotion](https://emotion.sh/docs/introduction), [styled-components](https://styled-components.com/) that generate styles during runtime) to zero runtime styling solutions ([tailwind](https://tailwindcss.com/), [vanilla-extract](https://github.com/seek-oss/vanilla-extract), [CSS modules](https://github.com/css-modules/css-modules) which generate styles during build time).\n\n### Children as the most basic optimization\n\n- The `children` prop is the most basic and easiest way to optimize your components. When applied properly, it eliminates a lot of unnecessary rerenders. The JSX, passed in the form of `children` prop, represents an isolated VDOM structure that does not need (and cannot) be re-rendered by its parent. Example below:\n\n```javascript\n// Not optimized example\nconst App = () => <Counter />;\n\nconst Counter = () => {\n  const [count, setCount] = useState(0);\n\n  return (\n    <div>\n      <button onClick={() => setCount((count) => count + 1)}>\n        count is {count}\n      </button>\n      <PureComponent /> // will rerender whenever \"count\" updates\n    </div>\n  );\n};\n\nconst PureComponent = () => <p>Pure Component</p>;\n\n// Optimized example\nconst App = () => (\n  <Counter>\n    <PureComponent />\n  </Counter>\n);\n\nconst Counter = ({ children }) => {\n  const [count, setCount] = useState(0);\n\n  return (\n    <div>\n      <button onClick={() => setCount((count) => count + 1)}>\n        count is {count}\n      </button>\n      {children} // won't rerender whenever \"count\" updates\n    </div>\n  );\n};\n\nconst PureComponent = () => <p>Pure Component</p>;\n```\n\n### Image optimizations\n\nConsider lazy loading images that are not in the viewport.\n\nUse modern image formats such as WEBP for faster image loading.\n\nUse `srcset` to load the most optimal image for the clients screen size.\n\n### Web vitals\n\nSince Google started taking web vitals in account when indexing websites, you should keep an eye on web vitals scores from [Lighthouse](https://web.dev/measure/) and [Pagespeed Insights](https://pagespeed.web.dev/).\n\n### Data prefetching\n\nIt is possible to prefetch data before the user navigates to a page. This can be done by using the `queryClient.prefetchQuery` method from the `@tanstack/react-query` library. This method allows you to prefetch data for a specific query. This can be useful when you know that the user will navigate to a specific page and you want to prefetch the data before the user navigates to the page. This can help to improve the performance of the application by reducing the time it takes to load the data when the user navigates to the page.\n\n[Data Prefetching Example Code](../apps/react-vite/src/features/discussions/components/discussions-list.tsx)\n"
  },
  {
    "path": "docs/project-standards.md",
    "content": "# ⚙️ Project Standards\n\nEnforcing project standards is crucial for maintaining code quality, consistency, and scalability in a React application. By establishing and adhering to a set of best practices, developers can ensure that the codebase remains clean, organized, and easy to maintain.\n\n#### ESLint\n\nESLint serves as a valuable linting tool for JavaScript, helping developers in maintaining code quality and adhering to coding standards. By configuring rules in the `.eslintrc.js` file, ESLint helps identify and prevent common errors, ensuring code correctness and promoting consistency throughout the codebase. This approach not only helps in catching mistakes early but also enforces uniformity in coding practices, thereby enhancing the overall quality and readability of the code.\n\n[ESLint Configuration Example Code](../apps/react-vite/.eslintrc.cjs)\n\n#### Prettier\n\nPrettier is a useful tool for maintaining consistent code formatting in your project. By enabling the \"format on save\" feature in your IDE, code is automatically formatted according to the rules set in the `.prettierrc` configuration file. This practice ensures a uniform code style across your codebase and provides helpful feedback on code issues. If the auto-formatting fails, it signals potential syntax error. Furthermore, Prettier can be integrated with ESLint to handle code formatting tasks alongside enforcing coding standards effectively throughout the development process.\n\n[Prettier Configuration Example Code](../apps/react-vite/.prettierrc)\n\n#### TypeScript\n\nESLint is effective for detecting language-related bugs in JavaScript. However, due to JavaScript's dynamic nature, ESLint may not catch all runtime data issues, especially in complex projects. To address this, TypeScript is recommended. TypeScript is valuable for identifying issues during large refactoring processes that may go unnoticed. When refactoring, prioritize updating type declarations first, then resolving TypeScript errors throughout the project. It's important to note that while TypeScript enhances development confidence by performing type checking at build time, it does not prevent runtime failures. Here is a [great resource on using TypeScript with React](https://react-typescript-cheatsheet.netlify.app/).\n\n#### Husky\n\nHusky is a valuable tool for implementing and executing git hooks in your workflow. By utilizing Husky to run code validations before each commit, you can ensure that your code maintains high standards and that no faulty commits are pushed to the repository. Husky enables you to perform various tasks such as linting, code formatting, and type checking before allowing code pushes. You can check how to configure it [here](https://typicode.github.io/husky/#/?id=usage).\n\n#### Absolute imports\n\nAbsolute imports should always be configured and used because it makes it easier to move files around and avoid messy import paths such as `../../../component`. Wherever you move the file, all the imports will remain intact. Here is how to configure it:\n\nFor JavaScript (`jsconfig.json`) projects:\n\n```json\n\"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"./src/*\"]\n    }\n  }\n```\n\nFor TypeScript (`tsconfig.json`) projects:\n\n```json\n\"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"./src/*\"]\n    }\n  }\n```\n\nIt is also possible to define multiple paths for various folders(such as `@components`, `@hooks`, etc.), but using `@/*` works very well because it is short enough so there is no need to configure multiple paths and it differs from other dependency modules so there is no confusion in what comes from `node_modules` and what is our source folder. That means that anything in the `src` folder can be accessed via `@`, e.g some file that lives in `src/components/my-component` can be accessed using `@/components/my-component` instead of `../../../components/my-component`.\n\n#### File naming conventions\n\nWe can also enforce the file naming conventions and folder naming conventions in the project. For example, you can enforce that all files should be named in `kebab-case`. This can help you to keep your codebase consistent and easier to navigate.\n\nTo enforce this, you can use ESLint:\n\n```js\n'check-file/filename-naming-convention': [\n  'error',\n  {\n      '**/*.{ts,tsx}': 'KEBAB_CASE',\n  },\n  {\n      // ignore the middle extensions of the filename to support filename like bable.config.js or smoke.spec.ts\n      ignoreMiddleExtensions: true,\n  },\n],\n'check-file/folder-naming-convention': [\n  'error',\n  {\n    // all folders within src (except __tests__)should be named in kebab-case\n    'src/**/!(__tests__)': 'KEBAB_CASE',\n  },\n],\n```\n"
  },
  {
    "path": "docs/project-structure.md",
    "content": "# 🗄️ Project Structure\n\nMost of the code lives in the `src` folder and looks something like this:\n\n```sh\nsrc\n|\n+-- app               # application layer containing:\n|   |                 # this folder might differ based on the meta framework used\n|   +-- routes        # application routes / can also be pages\n|   +-- app.tsx       # main application component\n|   +-- provider.tsx  # application provider that wraps the entire application with different global providers - this might also differ based on meta framework used\n|   +-- router.tsx    # application router configuration\n+-- assets            # assets folder can contain all the static files such as images, fonts, etc.\n|\n+-- components        # shared components used across the entire application\n|\n+-- config            # global configurations, exported env variables etc.\n|\n+-- features          # feature based modules\n|\n+-- hooks             # shared hooks used across the entire application\n|\n+-- lib               # reusable libraries preconfigured for the application\n|\n+-- stores            # global state stores\n|\n+-- testing           # test utilities and mocks\n|\n+-- types             # shared types used across the application\n|\n+-- utils             # shared utility functions\n```\n\nFor easy scalability and maintenance, organize most of the code within the features folder. Each feature folder should contain code specific to that feature, keeping things neatly separated. This approach helps prevent mixing feature-related code with shared components, making it simpler to manage and maintain the codebase compared to having many files in a flat folder structure. By adopting this method, you can enhance collaboration, readability, and scalability in the application's architecture.\n\nA feature could have the following structure:\n\n```sh\nsrc/features/awesome-feature\n|\n+-- api         # exported API request declarations and api hooks related to a specific feature\n|\n+-- assets      # assets folder can contain all the static files for a specific feature\n|\n+-- components  # components scoped to a specific feature\n|\n+-- hooks       # hooks scoped to a specific feature\n|\n+-- stores      # state stores for a specific feature\n|\n+-- types       # typescript types used within the feature\n|\n+-- utils       # utility functions for a specific feature\n```\n\nNOTE: You don't need all of these folders for every feature. Only include the ones that are necessary for the feature.\n\nIn some cases it might be more practical to keep all API calls outside of the features folders in a dedicated `api` folder where all API calls are defined. This can be useful if you have a lot of shared API calls between features.\n\nIn the past, it was recommended to use barrel files to export all the files from a feature. However, it can cause issues for Vite to do tree shaking and can lead to performance issues. Therefore, it is recommended to import the files directly.\n\nIt might not be a good idea to import across the features. Instead, compose different features at the application level. This way, you can ensure that each feature is independent which makes the codebase less convoluted.\n\nTo forbid cross-feature imports, you can use ESLint:\n\n```js\n'import/no-restricted-paths': [\n    'error',\n    {\n        zones: [\n            // disables cross-feature imports:\n            // eg. src/features/discussions should not import from src/features/comments, etc.\n            {\n                target: './src/features/auth',\n                from: './src/features',\n                except: ['./auth'],\n            },\n            {\n                target: './src/features/comments',\n                from: './src/features',\n                except: ['./comments'],\n            },\n            {\n                target: './src/features/discussions',\n                from: './src/features',\n                except: ['./discussions'],\n            },\n            {\n                target: './src/features/teams',\n                from: './src/features',\n                except: ['./teams'],\n            },\n            {\n                target: './src/features/users',\n                from: './src/features',\n                except: ['./users'],\n            },\n\n            // More restrictions...\n        ],\n    },\n],\n```\n\nYou might also want to enforce unidirectional codebase architecture. This means that the code should flow in one direction, from shared parts of the code to the application (shared -> features -> app). This is a good practice to follow as it makes the codebase more predictable and easier to understand.\n\n![Unidirectional Codebase](./assets/unidirectional-codebase.png)\n\nAs you can see, the shared parts can be used by any part of the codebase, but the features can only import from shared parts and the app can import from features and shared parts.\n\nTo enforce this, you can use ESLint:\n\n```js\n'import/no-restricted-paths': [\n    'error',\n    {\n    zones: [\n        // Previous restrictions...\n\n        // enforce unidirectional codebase:\n        // e.g. src/app can import from src/features but not the other way around\n        {\n            target: './src/features',\n            from: './src/app',\n        },\n\n        // e.g src/features and src/app can import from these shared modules but not the other way around\n        {\n            target: [\n                './src/components',\n                './src/hooks',\n                './src/lib',\n                './src/types',\n                './src/utils',\n            ],\n            from: ['./src/features', './src/app'],\n        },\n    ],\n    },\n],\n```\n\nBy following these practices, you can ensure that your codebase is well-organized, scalable, and maintainable. This will help you and your team to work more efficiently and effectively on the project.\nThis approach can also make it easier to apply similar architecture to apps built with Next.js, Remix or React Native.\n"
  },
  {
    "path": "docs/security.md",
    "content": "# 🔐 Security\n\n## Auth\n\nNOTE: While managing authentication on the client side is crucial, it is equally vital to implement robust security measures on the server to protect resources. Client-side authentication enhances user experience and complements server-side security measures.\n\nProtecting resources comprises two key components:\n\n### Authentication\n\nAuthentication is the process of verifying the identity of a user. In single-page applications (SPAs), the prevalent method of authenticating users is through JSON Web Tokens ([JWT](https://jwt.io/)). When a user logs in or registers, they receive a token that is stored within the application. Subsequently, for each authenticated request, the token is sent in the header or via a cookie along with the request to validate the user's identity and access permissions.\n\nThe most secure practice is to store the token in the application state. However, it's important to note that if the user refreshes the application, the token will be reset. That can lead to the loss of the user's authentication status.\n\nThat is why tokens need to be stored in a cookie or `localStorage/sessionStorage`.\n\n#### `localStorage` vs cookie for storing tokens\n\nStoring authentication tokens in localStorage can pose a security risk, especially in the context of Cross-Site Scripting ([XSS](https://owasp.org/www-community/attacks/xss/)) vulnerabilities, potentially leading to token theft by malicious actors.\n\nOpting to store tokens in cookies, configured with the `HttpOnly` attribute, can enhance security as they are inaccessible to client-side JavaScript. In our sample app, we utilize js-cookie for cookie management, assuming the real API would enforce the HttpOnly attribute for enhanced security, and the application does not have access to the cookie from the client side.\n\nIn addition to securely storing tokens, it's crucial to protect the entire application from Cross-Site Scripting (XSS) attacks. One key strategy is to sanitize all user inputs before displaying them in the application. By carefully sanitizing inputs, you can reduce the risk of XSS vulnerabilities, making the application more resilient to malicious attacks and enhancing overall security for users.\n\n[HTML Sanitization Example Code](../apps/react-vite/src/components/ui/md-preview/md-preview.tsx)\n\nFor a full list of security risks, check [OWASP](https://owasp.org/www-project-top-10-client-side-security-risks/).\n\n#### Handling user data\n\nUser info should be considered a global piece of state which should be available from anywhere in the application.\nIf you are already using `react-query`, you can use [react-query-auth](https://github.com/alan2207/react-query-auth) library for handling user state which will handle all the things for you after you provide it some configuration. Otherwise, you can use react context + hooks, or some 3rd party state management library.\n\nUser information should be treated as a central piece of data accessible throughout the application. If you are already using `react-query`, consider using it for storing user data as well. Alternatively, you can leverage React context with hooks or opt for a third-party state management library to efficiently manage user state across your application.\n\n[Auth Configuration Example Code](../apps/react-vite/src/lib/auth.tsx)\n\nThe application will assume the user is authenticated if a user object is present.\n\n### Authorization\n\nAuthorization is the process of verifying whether a user has permission to access a specific resource within the application.\n\n#### RBAC (Role based access control)\n\n[Authorization Configuration Example Code](../apps/react-vite/src/lib/authorization.tsx)\n\nIn a role-based authorization model, access to resources is determined by defining specific roles and associating them with permissions. For example, roles such as `USER` and `ADMIN` can be assigned different levels of access rights within the application. Users are then granted access based on their roles; for instance, restricting certain functionalities to regular users while permitting administrators to access all features and functionalities.\n\n[RBAC Example Code](../apps/react-vite/src/features/discussions/components/create-discussion.tsx)\n\n#### PBAC (Permission based access control)\n\nWhile Role-Based Access Control (RBAC) provides a structured methodology for authorization, there are instances where a more granular approach is necessary. Permission-Based Access Control (PBAC) offers a more flexible solution, particularly in scenarios where access permissions need to be finely tuned based on specific criteria, such as allowing only the owner of a resource to perform certain operations. For example, in the case of a user's comment, PBAC ensures that only the author of the comment has the privilege to delete it, adding a layer of precision and customization to access control mechanisms.\n\nFor RBAC protection, you can use the `RBAC` component by passing allowed roles to it. On the other hand, if you need more strict protection, you can pass policies check to it.\n\n[PBAC Example Code](../apps/react-vite/src/features/comments/components/comments-list.tsx)\n"
  },
  {
    "path": "docs/state-management.md",
    "content": "# 🗃️ State Management\n\nManaging state effectively is crucial for optimizing your application's performance. Instead of storing all state information in a single centralized repository, consider dividing it into various categories based on their usage. By categorizing your state, you can streamline your state management process and enhance your application's overall efficiency.\n\n## Component State\n\nComponent state is specific to individual components and should not be shared globally. It can be passed down to child components as props when necessary. Typically, you should begin by defining state within the component itself and consider elevating it to a higher level if it's required elsewhere in the application. When managing component state, you can use the following React hooks:\n\n- [useState](https://react.dev/reference/react/useState) - for simpler states that are independent\n- [useReducer](https://react.dev/reference/react/useReducer) - for more complex states where on a single action you want to update several pieces of state\n\n[Component State Example Code](../apps/react-vite/src/components/layouts/dashboard-layout.tsx)\n\n## Application State\n\nApplication state manages global parts of an application, such as controlling global modals, notifications, and toggling color modes. To ensure optimal performance and ease of maintenance, it is advisable to localize the state as closely as possible to the components that require it. Avoid unnecessarily globalizing all state variables from the outset to maintain a structured and efficient state management architecture.\n\nGood Application State Solutions:\n\n- [context](https://react.dev/learn/passing-data-deeply-with-context) + [hooks](https://react.dev/reference/react-dom/hooks)\n- [redux](https://redux.js.org/) + [redux toolkit](https://redux-toolkit.js.org/)\n- [mobx](https://mobx.js.org)\n- [zustand](https://github.com/pmndrs/zustand)\n- [jotai](https://github.com/pmndrs/jotai)\n- [xstate](https://xstate.js.org/)\n\n[Global State Example Code](../apps/react-vite/src/components/ui/notifications/notifications-store.ts)\n\n## Server Cache State\n\nThe Server Cache State refers to the data retrieved from the server that is stored locally on the client-side for future use. While it is feasible to cache remote data within a state management store like Redux, there exist more optimal solutions to this practice. It is essential to consider more efficient caching mechanisms to enhance performance and optimize data retrieval processes.\n\nGood Server Cache Libraries:\n\n- [react-query](https://tanstack.com/query) - REST + GraphQL\n- [swr](https://swr.vercel.app/) - REST + GraphQL\n- [apollo client](https://www.apollographql.com/) - GraphQL\n- [urql](https://formidable.com/open-source/urql/) - GraphQl\n- [RTK](https://redux-toolkit.js.org/rtk-query)\n\n[Server Cache State Example Code](../apps/react-vite/src/features/discussions/api/get-discussions.ts)\n\n## Form State\n\nForms are a crucial part of any application, and managing form state effectively is essential for a seamless user experience. When handling form state, consider using libraries like Formik, React Hook Form, or Final Form to streamline the process. These libraries provide built-in validation, error handling, and form submission functionalities, making it easier to manage form state within your application.\n\nForms in React can be [controlled and uncontrolled](https://react.dev/learn/sharing-state-between-components#controlled-and-uncontrolled-components).\n\nDepending on the application needs, they might be pretty complex with many different fields that require validation.\n\nAlthough it is possible to build any form using only React primitives, there are some good solutions out there that help with handling forms such as:\n\n- [React Hook Form](https://react-hook-form.com/)\n- [Formik](https://formik.org/)\n- [React Final Form](https://github.com/final-form/react-final-form)\n\nCreate abstracted `Form` component and all the input field components that wrap the library functionality and are adapted to the application needs.\n\n[Form Example Code](../apps/react-vite/src/components/ui/form/form.tsx)\n\n[Input Field Example Code](../apps/react-vite/src/components/ui/form/input.tsx)\n\nYou can also integrate validation libraries with the mentioned solutions to validate inputs on the client. Some good options are:\n\n- [zod](https://github.com/colinhacks/zod)\n- [yup](https://github.com/jquense/yup)\n\n[Validation Example Code](../apps/react-vite/src/features/auth/components/register-form.tsx)\n\n## URL State\n\nURL state refers to the data stored and manipulated within the address bar of the browser. This state is commonly managed through URL parameters (e.g., /app/${dynamicParam}) or query parameters (e.g., /app?dynamicParam=1). By incorporating routing solutions like react-router-dom, you can effectively access and control the URL state, enabling dynamic manipulation of application parameters directly from the browser's address bar.\n\n[URL State Example Code](../apps/react-vite/src/features/discussions/components/discussion-view.tsx)\n"
  },
  {
    "path": "docs/testing.md",
    "content": "# 🧪 Testing\n\nAs highlighted in this [tweet](https://twitter.com/rauchg/status/807626710350839808), the efficacy of testing lies in the comprehensive coverage provided by integration and end-to-end (e2e) tests. While unit tests serve a purpose in isolating and validating individual components, the true value and confidence in application functionality stem from robust integration and e2e testing strategies.\n\n## Types of tests:\n\n### Unit Tests\n\nUnit tests are the smallest tests you can write. They test individual parts of your application in isolation. They are useful for testing shared components and functions that are used throughout the entire application. They are also useful for testing complex logic in a single component. They are fast to run and easy to write.\n\n[Unit Test Example Code](../apps/react-vite/src/components/ui/dialog/confirmation-dialog/__tests__/confirmation-dialog.test.tsx)\n\n### Integration Tests\n\nIntegration testing checks how different parts of your application work together. It's crucial to focus on integration tests for most of your testing, as they provide significant benefits and boost confidence in your application's reliability. While unit tests are helpful for individual parts, passing them doesn't guarantee your app will function correctly if the connections between parts are flawed. Testing various features with integration tests is vital to ensure that your application works smoothly and consistently.\n\n[Integration Test Example Code](../apps/react-vite/src/app/routes/app/discussions/__tests__/discussion.test.tsx)\n\n### E2E\n\nEnd-to-End Testing is a method that evaluates an application as a whole. These tests involve automating the complete application, including both the frontend and backend, to confirm that the entire system functions correctly. End-to-End tests simulate how a user would interact with the application.\n\n[E2E Example Code](../apps/react-vite/e2e/tests/smoke.spec.ts)\n\n## Recommended Tooling:\n\n#### [Vitest](https://vitest.dev)\n\nVitest is a powerful testing framework with features similar to Jest, but it's more up-to-date and works well with modern tools. It's highly customizable and flexible, making it a popular option for testing JavaScript code.\n\n#### [Testing Library](https://testing-library.com/)\n\nTesting library is a set of libraries and tools that makes testing easier than ever before. Its philosophy is to test your app in a way it is being used by a real world user instead of testing implementation details. For example, don't test what is the current state value in a component, but test what that component renders on the screen for its user. If you refactor your app to use a different state management solution for example, the tests should still be relevant as the actual component output to the user shouldn't change.\n\n#### [Playwright](https://playwright.dev)\n\nPlaywright is a tool for running e2e tests in an automated way.\nYou define all the commands a real world user would execute when using the app and then start the test. It can be started in 2 modes:\n\n- Browser mode - it will open a dedicated browser and run your application from start to finish. You get a nice set of tools to visualize and inspect your application on each step. Since this is a more expensive option, you want to run it only locally when developing the application.\n- Headless mode - it will start a headless browser and run your application. Very useful for integrating with CI/CD to run it on every deploy.\n\n#### [MSW](https://mswjs.io)\n\nFor prototyping the API use msw, which is a great tool for quickly creating frontends without worrying about servers. It is not an actual backend, but a mocked server inside a service worker that intercepts all HTTP requests and returns desired responses based on the handlers you define. This is especially useful if you only have access to the frontend and are blocked by some not implemented features on the backend. This way, you will not be forced to wait for the feature to be completed or hardcode response data in the code, but use actual HTTP calls to build frontend features.\n\nIt can be used for designing API endpoints. The business logic of the mocked API can be created in its handlers.\n\n[API Handlers Example Code](../apps/react-vite/src/testing/mocks/handlers/auth.ts)\n\n[Data Models Example Code](../apps/react-vite/src/testing/mocks/db.ts)\n\nHaving a fully functional mocked API server is also handy when it comes to testing, you don't have to mock fetch, but make requests to the mocked server instead with the data your application would expect.\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"bulletproof-react\",\n  \"private\": true,\n  \"version\": \"1.0.0\",\n  \"description\": \"A simple, scalable, and powerful architecture for building production ready React applications.\",\n  \"scripts\": {\n    \"prepare\": \"cd ./apps/nextjs-app && yarn && cd ../nextjs-pages && yarn && cd ../react-vite && yarn\"\n  },\n  \"author\": \"alan2207\",\n  \"license\": \"MIT\"\n}\n"
  }
]