[
  {
    "path": ".devcontainer/Dockerfile",
    "content": "FROM mcr.microsoft.com/devcontainers/javascript-node:20 \n"
  },
  {
    "path": ".devcontainer/compose.dev.yml",
    "content": "services:\n  workspace:\n    build: \n      dockerfile: Dockerfile\n    volumes:\n      - ../:/workspace:cached\n    command: /bin/sh -c \"while sleep 1000; do :; done\"  \n    depends_on:\n      - database\n      \n  database:\n    image: postgres:17.2-alpine\n    environment:\n      POSTGRES_DB: acme\n      POSTGRES_USER: postgres\n      POSTGRES_PASSWORD: root\n "
  },
  {
    "path": ".devcontainer/devcontainer.json",
    "content": "{\n  \"name\": \"Next.js Auth Template\",\n  \"dockerComposeFile\": [\"compose.dev.yml\"],\n  \"service\": \"workspace\",\n  \"workspaceFolder\": \"/workspace\",\n  \"postCreateCommand\": \"pnpm config set store-dir $HOME/.pnpm-store\",\n  \"postStartCommand\": \"pnpm install\",\n  \"forwardPorts\": [3000],\n  \"features\": {\n    \"ghcr.io/devcontainers-extra/features/pnpm\": \"latest\"\n  },\n  \"customizations\": {\n    \"vscode\": {\n      \"settings\": {\n        \"editor.codeActionsOnSave\": {\n          \"source.fixAll.eslint\": \"explicit\",\n          \"source.organizeImports\": \"explicit\",\n          \"source.removeUnusedImports\": \"explicit\"\n        },\n        \"editor.guides.bracketPairs\": \"active\",\n        \"editor.rulers\": [100],\n        \"typescript.tsdk\": \"node_modules/typescript/lib\"\n      },\n      \"extensions\": [\n        \"dsznajder.es7-react-js-snippets\",\n        \"eamodio.gitlens\",\n        \"esbenp.prettier-vscode\",\n        \"YoavBls.pretty-ts-errors\",\n        \"bradlc.vscode-tailwindcss\"\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": ".eslintrc.cjs",
    "content": "/** @type {import(\"eslint\").Linter.Config} */\nconst config = {\n  parser: \"@typescript-eslint/parser\",\n  parserOptions: {\n    project: true,\n  },\n  plugins: [\"@typescript-eslint\"],\n  extends: [\n    \"plugin:@next/next/recommended\",\n    \"plugin:@typescript-eslint/recommended-type-checked\",\n    \"plugin:@typescript-eslint/stylistic-type-checked\",\n  ],\n  rules: {\n    // These opinionated rules are enabled in stylistic-type-checked above.\n    // Feel free to reconfigure them to your own preference.\n    \"@typescript-eslint/array-type\": \"off\",\n    \"@typescript-eslint/consistent-type-definitions\": \"off\",\n    \"@typescript-eslint/no-empty-interface\": \"off\",\n\n    \"@typescript-eslint/consistent-type-imports\": [\n      \"warn\",\n      {\n        prefer: \"type-imports\",\n        fixStyle: \"inline-type-imports\",\n      },\n    ],\n    \"@typescript-eslint/no-unused-vars\": [\"warn\", { argsIgnorePattern: \"^_\" }],\n    \"@typescript-eslint/require-await\": \"off\",\n    \"@typescript-eslint/no-misused-promises\": [\n      \"error\",\n      {\n        checksVoidReturn: { attributes: false },\n      },\n    ],\n  },\n  ignorePatterns: [\"*.js\"],\n};\n\nmodule.exports = config;\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Desktop (please complete the following information):**\n - OS: [e.g. iOS]\n - Browser [e.g. chrome, safari]\n - Version [e.g. 22]\n\n**Smartphone (please complete the following information):**\n - Device: [e.g. iPhone6]\n - OS: [e.g. iOS8.1]\n - Browser [e.g. stock browser, safari]\n - Version [e.g. 22]\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "# Description\n\nPlease include a summary of the changes and the related issue. Please also include relevant motivation and context. List any dependencies that are required for this change.\n\nFixes # (issue)\n\n## Type of change\n- [ ] Bug fix (non-breaking change which fixes an issue)\n- [ ] New feature (non-breaking change which adds functionality)\n- [ ] This change requires a documentation update\n- [ ] This change requires installing new dependencies\n\n"
  },
  {
    "path": ".github/workflows/check.yaml",
    "content": "name: Lint & Test\n\non:\n  pull_request:\n    branches: [main]\n\nconcurrency:\n  group: ci-${{ github.ref }}\n  cancel-in-progress: true\n\npermissions:\n  contents: read\n\njobs:\n  typecheck-and-lint:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v2\n      - uses: pnpm/action-setup@v2\n        with:\n          version: 9\n      - uses: actions/setup-node@v3\n        with:\n          node-version: 20.x\n          cache: \"pnpm\"\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Type Check and Lint\n        run: pnpm run typecheck && pnpm run lint\n        env:\n          SKIP_ENV_VALIDATION: true\n\n  e2e-test:\n    needs: typecheck-and-lint\n    timeout-minutes: 60\n    runs-on: ubuntu-latest\n\n    env:\n      DATABASE_URL: ${{secrets.DATABASE_URL}}\n      DISCORD_CLIENT_ID: ${{secrets.DISCORD_CLIENT_ID}}\n      DISCORD_CLIENT_SECRET: ${{secrets.DISCORD_CLIENT_SECRET}}\n      MOCK_SEND_EMAIL: \"true\"\n      SMTP_HOST: host\n      SMTP_PORT: 587\n      SMTP_USER: user\n      SMTP_PASSWORD: password\n      NEXT_PUBLIC_APP_URL: http://localhost:3000\n      STRIPE_API_KEY: stripe_api_key\n      STRIPE_WEBHOOK_SECRET: stripe_webhook_secret\n      STRIPE_PRO_MONTHLY_PLAN_ID: stripe_pro_monthly_plan_id\n\n    steps:\n      - uses: actions/checkout@v2\n      - uses: pnpm/action-setup@v2\n        with:\n          version: 9\n      - uses: actions/setup-node@v3\n        with:\n          node-version: 20.x\n          cache: \"pnpm\"\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n      - name: Build the app\n        run: pnpm build\n      - name: Install Playwright Browsers\n        run: pnpm exec playwright install chromium --with-deps\n      - name: Run Playwright tests\n        run: pnpm exec playwright test\n      - uses: actions/upload-artifact@v4\n        if: always()\n        with:\n          name: playwright-report\n          path: playwright-report/\n          retention-days: 30\n"
  },
  {
    "path": ".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\n# database\n/prisma/db.sqlite\n/prisma/db.sqlite-journal\n\n# next.js\n/.next/\n/out/\nnext-env.d.ts\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\napplication.log\n\n# local env files\n# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables\n.env\n.env*.local\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\n/test-results/\n/playwright-report/\n/blob-report/\n/playwright/.cache/\ntests/e2e/output"
  },
  {
    "path": "LICENSE",
    "content": "# MIT License\n\nCopyright (c) [2023] [Touha Zohair]\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": "# Next.js Auth Starter Template\n\n## Motivation\n\nImplementing authentication in Next.js, especially Email+Password authentication, can be challenging. NextAuth intentionally limits email password functionality to discourage the use of passwords due to security risks and added complexity. However, in certain projects, clients may require user password authentication. Lucia offers a flexible alternative to NextAuth.js, providing more customization options without compromising on security. This template serves as a starting point for building a Next.js app with Lucia authentication.\n\n## Lucia vs. NextAuth.js\n\nLucia is less opinionated than NextAuth, offering greater flexibility for customization. While Lucia involves more setup, it provides a higher degree of flexibility, making it a suitable choice for projects requiring unique authentication configurations.\n\n## Key Features\n\n- **Authentication:** 💼 Support for Credential and OAuth authentication.\n- **Authorization:** 🔒 Easily manage public and protected routes within the `app directory`.\n- **Email Verification:** 📧 Verify user identities through email.\n- **Password Reset:** 🔑 Streamline password resets by sending email password reset links.\n- **Lucia + tRPC:** 🔄 Similar to NextAuth with tRPC, granting access to sessions and user information through tRPC procedures.\n- **E2E tests:** 🧪 Catch every issue before your users do with comprehensive E2E testing.\n- **Stripe Payment:** 💳 Setup user subscriptions seamlessly with stripe.\n- **Email template with react-email:** ✉️ Craft your email templates using React.\n- **PostgreSQL Database:** 🛢️ Utilize a PostgreSQL database set up using Drizzle for enhanced performance and type safety.\n- **Database Migration:** 🚀 Included migration script to extend the database schema according to your project needs.\n\n## Tech Stack\n\n- [Next.js](https://nextjs.org)\n- [Lucia](https://lucia-auth.com/)\n- [tRPC](https://trpc.io)\n- [Drizzle ORM](https://orm.drizzle.team/)\n- [PostgreSQL](https://www.postgresql.org/)\n- [Stripe](https://stripe.com/)\n- [Tailwind CSS](https://tailwindcss.com)\n- [Shadcn UI](https://ui.shadcn.com/)\n- [React Hook Form](https://www.react-hook-form.com/)\n- [React Email](https://react.email/)\n- [Playwright](https://playwright.dev/)\n\n## Get Started\n\n1. Clone this repository to your local machine.\n2. Copy `.env.example` to `.env` and fill in the required environment variables.\n3. Run `pnpm install` to install dependencies.\n4. `(for node v18 or lower):` Uncomment polyfills for `webCrypto` in `src/lib/auth/index.ts`\n5. Update app title, database prefix, and other parameters in the `src/lib/constants.ts` file.\n6. Run `pnpm db:push` to push your schema to the database.\n7. Execute `pnpm dev` to start the development server and enjoy!\n\n## Testing\n\n1. Install [Playwright](https://playwright.dev/) (use this command if you want to install chromium only `pnpm exec playwright install chromium --with-deps`)\n2. Build production files using `pnpm build`\n3. Run `pnpm test:e2e` (add --debug flag to open tests in browser in debug mode)\n\n## Using Github actions\n\nAdd the following environment variables to your **github actions repository secrets** -\n`DATABASE_URL`, `DISCORD_CLIENT_ID`, `DISCORD_CLIENT_SECRET`\n\n## Roadmap\n\n- [ ] Update Password\n- [x] Stripe Integration\n<!-- - [x] API Rate-Limiting see branch - [upstash-ratelimiting](https://github.com/iamtouha/next-lucia-auth/tree/upstash-ratelimiting) -->\n- [ ] Admin Dashboard (under consideration)\n- [ ] Role-Based Access Policy (under consideration)\n\n## Contributing\n\nTo contribute, fork the repository and create a feature branch. Test your changes, and if possible, open an issue for discussion before submitting a pull request. Follow project guidelines, and welcome feedback to ensure a smooth integration of your contributions. Your pull requests are warmly welcome.\n"
  },
  {
    "path": "components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"default\",\n  \"rsc\": true,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"config\": \"tailwind.config.ts\",\n    \"css\": \"src/styles/globals.css\",\n    \"baseColor\": \"zinc\",\n    \"cssVariables\": true\n  },\n  \"aliases\": {\n    \"components\": \"@/components\",\n    \"utils\": \"@/lib/utils\"\n  }\n}\n"
  },
  {
    "path": "drizzle.config.ts",
    "content": "import { defineConfig } from \"drizzle-kit\";\nimport { DATABASE_PREFIX } from \"@/lib/constants\";\n\nexport default defineConfig({\n  schema: \"./src/server/db/schema.ts\",\n  out: \"./drizzle\",\n  dialect: \"postgresql\",\n  dbCredentials: {\n    url: process.env.DATABASE_URL!,\n  },\n  tablesFilter: [`${DATABASE_PREFIX}_*`],\n});\n"
  },
  {
    "path": "next.config.js",
    "content": "await import(\"./src/env.js\");\n\n/** @type {import(\"next\").NextConfig} */\nconst config = {};\n\nexport default config;\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"next-lucia-auth\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"next build\",\n    \"db:push\": \"dotenv drizzle-kit push\",\n    \"db:generate\": \"dotenv drizzle-kit generate\",\n    \"db:migrate\": \"dotenv drizzle-kit migrate\",\n    \"db:studio\": \"dotenv drizzle-kit studio\",\n    \"dev\": \"next dev\",\n    \"start\": \"next start\",\n    \"lint\": \"next lint\",\n    \"typecheck\": \"tsc --noEmit\",\n    \"stripe:listen\": \"stripe listen --forward-to localhost:3000/api/webhooks/stripe --latest\",\n    \"test:e2e\": \"playwright test\"\n  },\n  \"dependencies\": {\n    \"@hookform/resolvers\": \"^3.9.0\",\n    \"@lucia-auth/adapter-drizzle\": \"1.0.7\",\n    \"@radix-ui/react-alert-dialog\": \"^1.1.1\",\n    \"@radix-ui/react-dialog\": \"^1.1.1\",\n    \"@radix-ui/react-dropdown-menu\": \"^2.1.1\",\n    \"@radix-ui/react-icons\": \"^1.3.0\",\n    \"@radix-ui/react-label\": \"^2.1.0\",\n    \"@radix-ui/react-slot\": \"^1.1.0\",\n    \"@radix-ui/react-tabs\": \"^1.1.0\",\n    \"@react-email/components\": \"^0.0.12\",\n    \"@react-email/render\": \"^0.0.10\",\n    \"@t3-oss/env-nextjs\": \"^0.7.3\",\n    \"@tanstack/react-query\": \"^4.36.1\",\n    \"@trpc/client\": \"^10.45.2\",\n    \"@trpc/next\": \"^10.45.2\",\n    \"@trpc/react-query\": \"^10.45.2\",\n    \"@trpc/server\": \"^10.45.2\",\n    \"arctic\": \"^1.9.2\",\n    \"class-variance-authority\": \"^0.7.0\",\n    \"clsx\": \"^2.1.1\",\n    \"lucia\": \"3.2.0\",\n    \"next\": \"^14.2.5\",\n    \"next-themes\": \"^0.2.1\",\n    \"nodemailer\": \"^6.9.14\",\n    \"oslo\": \"^1.2.1\",\n    \"postgres\": \"^3.4.4\",\n    \"react\": \"18.2.0\",\n    \"react-dom\": \"18.2.0\",\n    \"react-hook-form\": \"^7.52.1\",\n    \"react-markdown\": \"^9.0.1\",\n    \"react-syntax-highlighter\": \"^15.5.0\",\n    \"rehype-raw\": \"^7.0.0\",\n    \"remark-gfm\": \"^4.0.0\",\n    \"server-only\": \"^0.0.1\",\n    \"sonner\": \"^1.5.0\",\n    \"stripe\": \"^14.25.0\",\n    \"superjson\": \"^2.2.1\",\n    \"tailwind-merge\": \"^2.4.0\",\n    \"tailwindcss-animate\": \"^1.0.7\",\n    \"vaul\": \"^0.8.9\",\n    \"zod\": \"^3.23.8\"\n  },\n  \"devDependencies\": {\n    \"@next/eslint-plugin-next\": \"^14.2.5\",\n    \"@playwright/test\": \"^1.45.3\",\n    \"@tailwindcss/typography\": \"^0.5.13\",\n    \"@types/eslint\": \"^8.56.11\",\n    \"@types/node\": \"^18.19.42\",\n    \"@types/nodemailer\": \"^6.4.15\",\n    \"@types/react\": \"^18.3.3\",\n    \"@types/react-dom\": \"^18.3.0\",\n    \"@types/react-syntax-highlighter\": \"^15.5.13\",\n    \"@typescript-eslint/eslint-plugin\": \"^6.21.0\",\n    \"@typescript-eslint/parser\": \"^6.21.0\",\n    \"autoprefixer\": \"^10.4.19\",\n    \"dotenv\": \"^16.4.5\",\n    \"dotenv-cli\": \"^7.4.2\",\n    \"drizzle-kit\": \"^0.23.0\",\n    \"drizzle-orm\": \"^0.32.1\",\n    \"eslint\": \"^8.57.0\",\n    \"pg\": \"^8.12.0\",\n    \"postcss\": \"^8.4.40\",\n    \"prettier\": \"^3.3.3\",\n    \"prettier-plugin-tailwindcss\": \"^0.5.14\",\n    \"tailwindcss\": \"^3.4.7\",\n    \"tsx\": \"^4.16.2\",\n    \"typescript\": \"^5.5.4\"\n  },\n  \"ct3aMetadata\": {\n    \"initVersion\": \"7.24.2\"\n  }\n}\n"
  },
  {
    "path": "playwright.config.ts",
    "content": "import { defineConfig, devices } from \"@playwright/test\";\nimport \"dotenv/config\";\n\nconst baseURL = `http://localhost:${process.env.PORT ?? 3000}`;\n\nexport default defineConfig({\n  testDir: \"./tests/e2e\",\n  outputDir: \"./tests/e2e/output\",\n  timeout: 60 * 1000,\n  fullyParallel: true,\n\n  forbidOnly: !!process.env.CI,\n  retries: process.env.CI ? 2 : 0,\n  workers: process.env.CI ? 1 : undefined,\n  reporter: \"html\",\n  use: {\n    trace: \"on-first-retry\",\n    baseURL,\n  },\n\n  projects: [\n    {\n      name: \"chromium\",\n      use: { ...devices[\"Desktop Chrome\"] },\n    },\n  ],\n  webServer: {\n    command: \"npx cross-env NODE_ENV=test npm run start\",\n    url: baseURL,\n    stdout: \"pipe\",\n    stderr: \"pipe\",\n    reuseExistingServer: !process.env.CI,\n  },\n});\n"
  },
  {
    "path": "postcss.config.cjs",
    "content": "const config = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n};\n\nmodule.exports = config;\n"
  },
  {
    "path": "prettier.config.js",
    "content": "/** @type {import('prettier').Config & import('prettier-plugin-tailwindcss').PluginOptions} */\nconst config = {\n  plugins: [\"prettier-plugin-tailwindcss\"],\n  tabWidth: 2,\n  semi: true,\n  singleQuote: false,\n  printWidth: 100,\n};\n\nexport default config;\n"
  },
  {
    "path": "src/app/(auth)/layout.tsx",
    "content": "import type { ReactNode } from \"react\";\n\nconst AuthLayout = ({ children }: { children: ReactNode }) => {\n  return (\n    <div className=\"grid min-h-screen place-items-center p-4\">{children}</div>\n  );\n};\n\nexport default AuthLayout;\n"
  },
  {
    "path": "src/app/(auth)/login/discord/callback/route.ts",
    "content": "import { cookies } from \"next/headers\";\nimport { generateId } from \"lucia\";\nimport { OAuth2RequestError } from \"arctic\";\nimport { eq } from \"drizzle-orm\";\nimport { discord, lucia } from \"@/lib/auth\";\nimport { db } from \"@/server/db\";\nimport { Paths } from \"@/lib/constants\";\nimport { users } from \"@/server/db/schema\";\n\nexport async function GET(request: Request): Promise<Response> {\n  const url = new URL(request.url);\n  const code = url.searchParams.get(\"code\");\n  const state = url.searchParams.get(\"state\");\n  const storedState = cookies().get(\"discord_oauth_state\")?.value ?? null;\n\n  if (!code || !state || !storedState || state !== storedState) {\n    return new Response(null, {\n      status: 400,\n      headers: { Location: Paths.Login },\n    });\n  }\n\n  try {\n    const tokens = await discord.validateAuthorizationCode(code);\n\n    const discordUserRes = await fetch(\"https://discord.com/api/users/@me\", {\n      headers: {\n        Authorization: `Bearer ${tokens.accessToken}`,\n      },\n    });\n    const discordUser = (await discordUserRes.json()) as DiscordUser;\n\n    if (!discordUser.email || !discordUser.verified) {\n      return new Response(\n        JSON.stringify({\n          error: \"Your Discord account must have a verified email address.\",\n        }),\n        { status: 400, headers: { Location: Paths.Login } },\n      );\n    }\n    const existingUser = await db.query.users.findFirst({\n      where: (table, { eq, or }) =>\n        or(eq(table.discordId, discordUser.id), eq(table.email, discordUser.email!)),\n    });\n\n    const avatar = discordUser.avatar\n      ? `https://cdn.discordapp.com/avatars/${discordUser.id}/${discordUser.avatar}.webp`\n      : null;\n\n    if (!existingUser) {\n      const userId = generateId(21);\n      await db.insert(users).values({\n        id: userId,\n        email: discordUser.email,\n        emailVerified: true,\n        discordId: discordUser.id,\n        avatar,\n      });\n      const session = await lucia.createSession(userId, {});\n      const sessionCookie = lucia.createSessionCookie(session.id);\n      cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);\n      return new Response(null, {\n        status: 302,\n        headers: { Location: Paths.Dashboard },\n      });\n    }\n\n    if (existingUser.discordId !== discordUser.id || existingUser.avatar !== avatar) {\n      await db\n        .update(users)\n        .set({\n          discordId: discordUser.id,\n          emailVerified: true,\n          avatar,\n        })\n        .where(eq(users.id, existingUser.id));\n    }\n    const session = await lucia.createSession(existingUser.id, {});\n    const sessionCookie = lucia.createSessionCookie(session.id);\n    cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);\n    return new Response(null, {\n      status: 302,\n      headers: { Location: Paths.Dashboard },\n    });\n  } catch (e) {\n    // the specific error message depends on the provider\n    if (e instanceof OAuth2RequestError) {\n      // invalid code\n      return new Response(JSON.stringify({ message: \"Invalid code\" }), {\n        status: 400,\n      });\n    }\n    console.error(e);\n\n    return new Response(JSON.stringify({ message: \"internal server error\" }), {\n      status: 500,\n    });\n  }\n}\n\ninterface DiscordUser {\n  id: string;\n  username: string;\n  avatar: string | null;\n  banner: string | null;\n  global_name: string | null;\n  banner_color: string | null;\n  mfa_enabled: boolean;\n  locale: string;\n  email: string | null;\n  verified: boolean;\n}\n"
  },
  {
    "path": "src/app/(auth)/login/discord/route.ts",
    "content": "import { cookies } from \"next/headers\";\nimport { generateState } from \"arctic\";\nimport { discord } from \"@/lib/auth\";\nimport { env } from \"@/env\";\n\nexport async function GET(): Promise<Response> {\n  const state = generateState();\n  const url = await discord.createAuthorizationURL(state, {\n    scopes: [\"identify\", \"email\"],\n  });\n\n  cookies().set(\"discord_oauth_state\", state, {\n    path: \"/\",\n    secure: env.NODE_ENV === \"production\",\n    httpOnly: true,\n    maxAge: 60 * 10,\n    sameSite: \"lax\",\n  });\n\n  return Response.redirect(url);\n}\n"
  },
  {
    "path": "src/app/(auth)/login/login.tsx",
    "content": "\"use client\";\n\nimport Link from \"next/link\";\nimport { useFormState } from \"react-dom\";\nimport { Input } from \"@/components/ui/input\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { PasswordInput } from \"@/components/password-input\";\nimport { DiscordLogoIcon } from \"@/components/icons\";\nimport { APP_TITLE } from \"@/lib/constants\";\nimport { login } from \"@/lib/auth/actions\";\nimport { Label } from \"@/components/ui/label\";\nimport { SubmitButton } from \"@/components/submit-button\";\n\nexport function Login() {\n  const [state, formAction] = useFormState(login, null);\n\n  return (\n    <Card className=\"w-full max-w-md\">\n      <CardHeader className=\"text-center\">\n        <CardTitle>{APP_TITLE} Log In</CardTitle>\n        <CardDescription>Log in to your account to access your dashboard</CardDescription>\n      </CardHeader>\n      <CardContent>\n        <Button variant=\"outline\" className=\"w-full\" asChild>\n          <Link href=\"/login/discord\" prefetch={false}>\n            <DiscordLogoIcon className=\"mr-2 h-5 w-5\" />\n            Log in with Discord\n          </Link>\n        </Button>\n        <div className=\"my-2 flex items-center\">\n          <div className=\"flex-grow border-t border-muted\" />\n          <div className=\"mx-2 text-muted-foreground\">or</div>\n          <div className=\"flex-grow border-t border-muted\" />\n        </div>\n        <form action={formAction} className=\"grid gap-4\">\n          <div className=\"space-y-2\">\n            <Label htmlFor=\"email\">Email</Label>\n            <Input\n              required\n              id=\"email\"\n              placeholder=\"email@example.com\"\n              autoComplete=\"email\"\n              name=\"email\"\n              type=\"email\"\n            />\n          </div>\n\n          <div className=\"space-y-2\">\n            <Label htmlFor=\"password\">Password</Label>\n            <PasswordInput\n              id=\"password\"\n              name=\"password\"\n              required\n              autoComplete=\"current-password\"\n              placeholder=\"********\"\n            />\n          </div>\n\n          <div className=\"flex flex-wrap justify-between\">\n            <Button variant={\"link\"} size={\"sm\"} className=\"p-0\" asChild>\n              <Link href={\"/signup\"}>Not signed up? Sign up now.</Link>\n            </Button>\n            <Button variant={\"link\"} size={\"sm\"} className=\"p-0\" asChild>\n              <Link href={\"/reset-password\"}>Forgot password?</Link>\n            </Button>\n          </div>\n\n          {state?.fieldError ? (\n            <ul className=\"list-disc space-y-1 rounded-lg border bg-destructive/10 p-2 text-[0.8rem] font-medium text-destructive\">\n              {Object.values(state.fieldError).map((err) => (\n                <li className=\"ml-4\" key={err}>\n                  {err}\n                </li>\n              ))}\n            </ul>\n          ) : state?.formError ? (\n            <p className=\"rounded-lg border bg-destructive/10 p-2 text-[0.8rem] font-medium text-destructive\">\n              {state?.formError}\n            </p>\n          ) : null}\n          <SubmitButton className=\"w-full\" aria-label=\"submit-btn\">\n            Log In\n          </SubmitButton>\n          <Button variant=\"outline\" className=\"w-full\" asChild>\n            <Link href=\"/\">Cancel</Link>\n          </Button>\n        </form>\n      </CardContent>\n    </Card>\n  );\n}\n"
  },
  {
    "path": "src/app/(auth)/login/page.tsx",
    "content": "import { redirect } from \"next/navigation\";\nimport { validateRequest } from \"@/lib/auth/validate-request\";\nimport { Paths } from \"@/lib/constants\";\nimport { Login } from \"./login\";\n\nexport const metadata = {\n  title: \"Login\",\n  description: \"Login Page\",\n};\n\nexport default async function LoginPage() {\n  const { user } = await validateRequest();\n\n  if (user) redirect(Paths.Dashboard);\n\n  return <Login />;\n}\n"
  },
  {
    "path": "src/app/(auth)/reset-password/[token]/page.tsx",
    "content": "import {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card\";\nimport { ResetPassword } from \"./reset-password\";\n\nexport const metadata = {\n  title: \"Reset Password\",\n  description: \"Reset Password Page\",\n};\n\nexport default function ResetPasswordPage({\n  params,\n}: {\n  params: { token: string };\n}) {\n  return (\n    <Card className=\"w-full max-w-md\">\n      <CardHeader className=\"space-y-1\">\n        <CardTitle>Reset password</CardTitle>\n        <CardDescription>Enter new password.</CardDescription>\n      </CardHeader>\n      <CardContent>\n        <ResetPassword token={params.token} />\n      </CardContent>\n    </Card>\n  );\n}\n"
  },
  {
    "path": "src/app/(auth)/reset-password/[token]/reset-password.tsx",
    "content": "\"use client\";\n\nimport { useEffect } from \"react\";\nimport { useFormState } from \"react-dom\";\nimport { toast } from \"sonner\";\nimport { ExclamationTriangleIcon } from \"@/components/icons\";\nimport { SubmitButton } from \"@/components/submit-button\";\nimport { PasswordInput } from \"@/components/password-input\";\nimport { Label } from \"@/components/ui/label\";\nimport { resetPassword } from \"@/lib/auth/actions\";\n\nexport function ResetPassword({ token }: { token: string }) {\n  const [state, formAction] = useFormState(resetPassword, null);\n\n  useEffect(() => {\n    if (state?.error) {\n      toast(state.error, {\n        icon: <ExclamationTriangleIcon className=\"h-5 w-5 text-destructive\" />,\n      });\n    }\n  }, [state?.error]);\n\n  return (\n    <form action={formAction} className=\"space-y-4\">\n      <input type=\"hidden\" name=\"token\" value={token} />\n      <div className=\"space-y-2\">\n        <Label>New Password</Label>\n        <PasswordInput\n          name=\"password\"\n          required\n          autoComplete=\"new-password\"\n          placeholder=\"********\"\n        />\n      </div>\n      <SubmitButton className=\"w-full\">Reset Password</SubmitButton>\n    </form>\n  );\n}\n"
  },
  {
    "path": "src/app/(auth)/reset-password/page.tsx",
    "content": "import { redirect } from \"next/navigation\";\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card\";\nimport { SendResetEmail } from \"./send-reset-email\";\nimport { validateRequest } from \"@/lib/auth/validate-request\";\nimport { Paths } from \"@/lib/constants\";\n\nexport const metadata = {\n  title: \"Forgot Password\",\n  description: \"Forgot Password Page\",\n};\n\nexport default async function ForgotPasswordPage() {\n  const { user } = await validateRequest();\n\n  if (user) redirect(Paths.Dashboard);\n\n  return (\n    <Card className=\"w-full max-w-md\">\n      <CardHeader>\n        <CardTitle>Forgot password?</CardTitle>\n        <CardDescription>\n          Password reset link will be sent to your email.\n        </CardDescription>\n      </CardHeader>\n      <CardContent>\n        <SendResetEmail />\n      </CardContent>\n    </Card>\n  );\n}\n"
  },
  {
    "path": "src/app/(auth)/reset-password/send-reset-email.tsx",
    "content": "\"use client\";\n\nimport { useEffect } from \"react\";\nimport { useFormState } from \"react-dom\";\nimport Link from \"next/link\";\nimport { useRouter } from \"next/navigation\";\nimport { toast } from \"sonner\";\nimport { Input } from \"@/components/ui/input\";\nimport { Button } from \"@/components/ui/button\";\nimport { Label } from \"@/components/ui/label\";\nimport { SubmitButton } from \"@/components/submit-button\";\nimport { sendPasswordResetLink } from \"@/lib/auth/actions\";\nimport { ExclamationTriangleIcon } from \"@/components/icons\";\nimport { Paths } from \"@/lib/constants\";\n\nexport function SendResetEmail() {\n  const [state, formAction] = useFormState(sendPasswordResetLink, null);\n  const router = useRouter();\n\n  useEffect(() => {\n    if (state?.success) {\n      toast(\"A password reset link has been sent to your email.\");\n      router.push(Paths.Login);\n    }\n    if (state?.error) {\n      toast(state.error, {\n        icon: <ExclamationTriangleIcon className=\"h-5 w-5 text-destructive\" />,\n      });\n    }\n  }, [state?.error, state?.success]);\n\n  return (\n    <form className=\"space-y-4\" action={formAction}>\n      <div className=\"space-y-2\">\n        <Label>Your Email</Label>\n        <Input\n          required\n          placeholder=\"email@example.com\"\n          autoComplete=\"email\"\n          name=\"email\"\n          type=\"email\"\n        />\n      </div>\n\n      <div className=\"flex flex-wrap justify-between\">\n        <Link href={Paths.Signup}>\n          <Button variant={\"link\"} size={\"sm\"} className=\"p-0\">\n            Not signed up? Sign up now\n          </Button>\n        </Link>\n      </div>\n\n      <SubmitButton className=\"w-full\">Reset Password</SubmitButton>\n      <Button variant=\"outline\" className=\"w-full\" asChild>\n        <Link href=\"/\">Cancel</Link>\n      </Button>\n    </form>\n  );\n}\n"
  },
  {
    "path": "src/app/(auth)/signup/page.tsx",
    "content": "import { redirect } from \"next/navigation\";\nimport { Signup } from \"./signup\";\nimport { validateRequest } from \"@/lib/auth/validate-request\";\nimport { Paths } from \"@/lib/constants\";\n\nexport const metadata = {\n  title: \"Sign Up\",\n  description: \"Signup Page\",\n};\n\nexport default async function SignupPage() {\n  const { user } = await validateRequest();\n\n  if (user) redirect(Paths.Dashboard);\n\n  return <Signup />;\n}\n"
  },
  {
    "path": "src/app/(auth)/signup/signup.tsx",
    "content": "\"use client\";\n\nimport { useFormState } from \"react-dom\";\nimport Link from \"next/link\";\nimport { PasswordInput } from \"@/components/password-input\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { Input } from \"@/components/ui/input\";\nimport { DiscordLogoIcon } from \"@/components/icons\";\nimport { APP_TITLE } from \"@/lib/constants\";\nimport { Label } from \"@/components/ui/label\";\nimport { signup } from \"@/lib/auth/actions\";\nimport { SubmitButton } from \"@/components/submit-button\";\n\nexport function Signup() {\n  const [state, formAction] = useFormState(signup, null);\n\n  return (\n    <Card className=\"w-full max-w-md\">\n      <CardHeader className=\"text-center\">\n        <CardTitle>{APP_TITLE} Sign Up</CardTitle>\n        <CardDescription>Sign up to start using the app</CardDescription>\n      </CardHeader>\n      <CardContent>\n        <Button variant=\"outline\" className=\"w-full\" asChild>\n          <Link href=\"/login/discord\" prefetch={false}>\n            <DiscordLogoIcon className=\"mr-2 h-5 w-5\" />\n            Sign up with Discord\n          </Link>\n        </Button>\n        <div className=\"my-2 flex items-center\">\n          <div className=\"flex-grow border-t border-muted\" />\n          <div className=\"mx-2 text-muted-foreground\">or</div>\n          <div className=\"flex-grow border-t border-muted\" />\n        </div>\n\n        <form action={formAction} className=\"space-y-4\">\n          <div className=\"space-y-2\">\n            <Label htmlFor=\"email\">Email</Label>\n            <Input\n              id=\"email\"\n              required\n              placeholder=\"email@example.com\"\n              autoComplete=\"email\"\n              name=\"email\"\n              type=\"email\"\n            />\n          </div>\n          <div className=\"space-y-2\">\n            <Label htmlFor=\"password\">Password</Label>\n            <PasswordInput\n              id=\"password\"\n              name=\"password\"\n              required\n              autoComplete=\"current-password\"\n              placeholder=\"********\"\n            />\n          </div>\n\n          {state?.fieldError ? (\n            <ul className=\"list-disc space-y-1 rounded-lg border bg-destructive/10 p-2 text-[0.8rem] font-medium text-destructive\">\n              {Object.values(state.fieldError).map((err) => (\n                <li className=\"ml-4\" key={err}>\n                  {err}\n                </li>\n              ))}\n            </ul>\n          ) : state?.formError ? (\n            <p className=\"rounded-lg border bg-destructive/10 p-2 text-[0.8rem] font-medium text-destructive\">\n              {state?.formError}\n            </p>\n          ) : null}\n          <div>\n            <Link href={\"/login\"}>\n              <span className=\"p-0 text-xs font-medium underline-offset-4 hover:underline\">\n                Already signed up? Login instead.\n              </span>\n            </Link>\n          </div>\n\n          <SubmitButton className=\"w-full\" aria-label=\"submit-btn\">\n            Sign Up\n          </SubmitButton>\n          <Button variant=\"outline\" className=\"w-full\" asChild>\n            <Link href=\"/\">Cancel</Link>\n          </Button>\n        </form>\n      </CardContent>\n    </Card>\n  );\n}\n"
  },
  {
    "path": "src/app/(auth)/verify-email/page.tsx",
    "content": "import {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card\";\nimport { redirect } from \"next/navigation\";\nimport { validateRequest } from \"@/lib/auth/validate-request\";\nimport { VerifyCode } from \"./verify-code\";\nimport { Paths } from \"@/lib/constants\";\n\nexport const metadata = {\n  title: \"Verify Email\",\n  description: \"Verify Email Page\",\n};\n\nexport default async function VerifyEmailPage() {\n  const { user } = await validateRequest();\n\n  if (!user) redirect(Paths.Login);\n  if (user.emailVerified) redirect(Paths.Dashboard);\n\n  return (\n    <Card className=\"w-full max-w-md\">\n      <CardHeader>\n        <CardTitle>Verify Email</CardTitle>\n        <CardDescription>\n          Verification code was sent to <strong>{user.email}</strong>. Check\n          your spam folder if you can't find the email.\n        </CardDescription>\n      </CardHeader>\n      <CardContent>\n        <VerifyCode />\n      </CardContent>\n    </Card>\n  );\n}\n"
  },
  {
    "path": "src/app/(auth)/verify-email/verify-code.tsx",
    "content": "\"use client\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@radix-ui/react-label\";\nimport { useEffect, useRef } from \"react\";\nimport { useFormState } from \"react-dom\";\nimport { toast } from \"sonner\";\nimport { ExclamationTriangleIcon } from \"@/components/icons\";\nimport { logout, verifyEmail, resendVerificationEmail as resendEmail } from \"@/lib/auth/actions\";\nimport { SubmitButton } from \"@/components/submit-button\";\n\nexport const VerifyCode = () => {\n  const [verifyEmailState, verifyEmailAction] = useFormState(verifyEmail, null);\n  const [resendState, resendAction] = useFormState(resendEmail, null);\n  const codeFormRef = useRef<HTMLFormElement>(null);\n\n  useEffect(() => {\n    if (resendState?.success) {\n      toast(\"Email sent!\");\n    }\n    if (resendState?.error) {\n      toast(resendState.error, {\n        icon: <ExclamationTriangleIcon className=\"h-5 w-5 text-destructive\" />,\n      });\n    }\n  }, [resendState?.error, resendState?.success]);\n\n  useEffect(() => {\n    if (verifyEmailState?.error) {\n      toast(verifyEmailState.error, {\n        icon: <ExclamationTriangleIcon className=\"h-5 w-5 text-destructive\" />,\n      });\n    }\n  }, [verifyEmailState?.error]);\n\n  return (\n    <div className=\"flex flex-col gap-2\">\n      <form ref={codeFormRef} action={verifyEmailAction}>\n        <Label htmlFor=\"code\">Verification Code</Label>\n        <Input className=\"mt-2\" type=\"text\" id=\"code\" name=\"code\" required />\n        <SubmitButton className=\"mt-4 w-full\" aria-label=\"submit-btn\">\n          Verify\n        </SubmitButton>\n      </form>\n      <form action={resendAction}>\n        <SubmitButton className=\"w-full\" variant=\"secondary\">\n          Resend Code\n        </SubmitButton>\n      </form>\n      <form action={logout}>\n        <SubmitButton variant=\"link\" className=\"p-0 font-normal\">\n          want to use another email? Log out now.\n        </SubmitButton>\n      </form>\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/app/(landing)/_components/copy-to-clipboard.tsx",
    "content": "\"use client\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { cn } from \"@/lib/utils\";\nimport { CheckIcon, CopyIcon } from \"@radix-ui/react-icons\";\nimport { useState } from \"react\";\nimport { toast } from \"sonner\";\n\nexport const CopyToClipboard = ({ text }: { text: string }) => {\n  const [copied, setCopied] = useState(false);\n  const copyToClipboard = async () => {\n    setCopied(true);\n    setTimeout(() => {\n      setCopied(false);\n    }, 2000);\n    await navigator.clipboard.writeText(text);\n    toast(\"Copied to clipboard\", {\n      icon: <CopyIcon className=\"h-4 w-4\" />,\n    });\n  };\n  return (\n    <div className=\"flex justify-center gap-3\">\n      <Input readOnly value={text} className=\"bg-secondary text-muted-foreground\" />\n      <Button size=\"icon\" onClick={() => copyToClipboard()}>\n        {copied ? (\n          <CheckIcon\n            className={cn(\n              copied ? \"opacity-100\" : \"opacity-0\",\n              \"h-5 w-5 transition-opacity duration-500\",\n            )}\n          />\n        ) : (\n          <CopyIcon className=\"h-5 w-5\" />\n        )}\n      </Button>\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/app/(landing)/_components/feature-icons.tsx",
    "content": "import { forwardRef, type SVGProps } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst NextjsLight = forwardRef<SVGSVGElement, SVGProps<SVGSVGElement>>(\n  ({ className, ...props }, ref) => (\n    <svg\n      ref={ref}\n      {...props}\n      viewBox=\"0 0 180 180\"\n      className={cn(className)}\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <mask\n        id=\"mask0_408_134\"\n        style={{ maskType: \"alpha\" }}\n        maskUnits=\"userSpaceOnUse\"\n        x=\"0\"\n        y=\"0\"\n        width=\"180\"\n        height=\"180\"\n      >\n        <circle cx=\"90\" cy=\"90\" r=\"90\" fill=\"black\" />\n      </mask>\n      <g mask=\"url(#mask0_408_134)\">\n        <circle cx=\"90\" cy=\"90\" r=\"90\" fill=\"black\" />\n        <path\n          d=\"M149.508 157.52L69.142 54H54V125.97H66.1136V69.3836L139.999 164.845C143.333 162.614 146.509 160.165 149.508 157.52Z\"\n          fill=\"url(#paint0_linear_408_134)\"\n        />\n        <rect x=\"115\" y=\"54\" width=\"12\" height=\"72\" fill=\"url(#paint1_linear_408_134)\" />\n      </g>\n      <defs>\n        <linearGradient\n          id=\"paint0_linear_408_134\"\n          x1=\"109\"\n          y1=\"116.5\"\n          x2=\"144.5\"\n          y2=\"160.5\"\n          gradientUnits=\"userSpaceOnUse\"\n        >\n          <stop stopColor=\"white\" />\n          <stop offset=\"1\" stopColor=\"white\" stopOpacity=\"0\" />\n        </linearGradient>\n        <linearGradient\n          id=\"paint1_linear_408_134\"\n          x1=\"121\"\n          y1=\"54\"\n          x2=\"120.799\"\n          y2=\"106.875\"\n          gradientUnits=\"userSpaceOnUse\"\n        >\n          <stop stopColor=\"white\" />\n          <stop offset=\"1\" stopColor=\"white\" stopOpacity=\"0\" />\n        </linearGradient>\n      </defs>\n    </svg>\n  ),\n);\nNextjsLight.displayName = \"NextjsLight\";\n\nconst NextjsDark = forwardRef<SVGSVGElement, SVGProps<SVGSVGElement>>(\n  ({ className, ...props }, ref) => (\n    <svg\n      ref={ref}\n      {...props}\n      viewBox=\"0 0 180 180\"\n      className={cn(className)}\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <mask\n        id=\"mask0_408_139\"\n        style={{ maskType: \"alpha\" }}\n        maskUnits=\"userSpaceOnUse\"\n        x=\"0\"\n        y=\"0\"\n        width=\"180\"\n        height=\"180\"\n      >\n        <circle cx=\"90\" cy=\"90\" r=\"90\" fill=\"black\" />\n      </mask>\n      <g mask=\"url(#mask0_408_139)\">\n        <circle cx=\"90\" cy=\"90\" r=\"87\" fill=\"black\" stroke=\"white\" strokeWidth=\"6\" />\n        <path\n          d=\"M149.508 157.52L69.142 54H54V125.97H66.1136V69.3836L139.999 164.845C143.333 162.614 146.509 160.165 149.508 157.52Z\"\n          fill=\"url(#paint0_linear_408_139)\"\n        />\n        <rect x=\"115\" y=\"54\" width=\"12\" height=\"72\" fill=\"url(#paint1_linear_408_139)\" />\n      </g>\n      <defs>\n        <linearGradient\n          id=\"paint0_linear_408_139\"\n          x1=\"109\"\n          y1=\"116.5\"\n          x2=\"144.5\"\n          y2=\"160.5\"\n          gradientUnits=\"userSpaceOnUse\"\n        >\n          <stop stopColor=\"white\" />\n          <stop offset=\"1\" stopColor=\"white\" stopOpacity=\"0\" />\n        </linearGradient>\n        <linearGradient\n          id=\"paint1_linear_408_139\"\n          x1=\"121\"\n          y1=\"54\"\n          x2=\"120.799\"\n          y2=\"106.875\"\n          gradientUnits=\"userSpaceOnUse\"\n        >\n          <stop stopColor=\"white\" />\n          <stop offset=\"1\" stopColor=\"white\" stopOpacity=\"0\" />\n        </linearGradient>\n      </defs>\n    </svg>\n  ),\n);\nNextjsDark.displayName = \"NextjsDark\";\n\nconst ReactJs = forwardRef<SVGSVGElement, SVGProps<SVGSVGElement>>(\n  ({ className, ...props }, ref) => (\n    <svg\n      ref={ref}\n      {...props}\n      viewBox=\"0 0 24 24\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      fill=\"currentColor\"\n      className={cn(className)}\n    >\n      <path d=\"M14.23 12.004a2.236 2.236 0 0 1-2.235 2.236 2.236 2.236 0 0 1-2.236-2.236 2.236 2.236 0 0 1 2.235-2.236 2.236 2.236 0 0 1 2.236 2.236zm2.648-10.69c-1.346 0-3.107.96-4.888 2.622-1.78-1.653-3.542-2.602-4.887-2.602-.41 0-.783.093-1.106.278-1.375.793-1.683 3.264-.973 6.365C1.98 8.917 0 10.42 0 12.004c0 1.59 1.99 3.097 5.043 4.03-.704 3.113-.39 5.588.988 6.38.32.187.69.275 1.102.275 1.345 0 3.107-.96 4.888-2.624 1.78 1.654 3.542 2.603 4.887 2.603.41 0 .783-.09 1.106-.275 1.374-.792 1.683-3.263.973-6.365C22.02 15.096 24 13.59 24 12.004c0-1.59-1.99-3.097-5.043-4.032.704-3.11.39-5.587-.988-6.38a2.167 2.167 0 0 0-1.092-.278zm-.005 1.09v.006c.225 0 .406.044.558.127.666.382.955 1.835.73 3.704-.054.46-.142.945-.25 1.44a23.476 23.476 0 0 0-3.107-.534A23.892 23.892 0 0 0 12.769 4.7c1.592-1.48 3.087-2.292 4.105-2.295zm-9.77.02c1.012 0 2.514.808 4.11 2.28-.686.72-1.37 1.537-2.02 2.442a22.73 22.73 0 0 0-3.113.538 15.02 15.02 0 0 1-.254-1.42c-.23-1.868.054-3.32.714-3.707.19-.09.4-.127.563-.132zm4.882 3.05c.455.468.91.992 1.36 1.564-.44-.02-.89-.034-1.345-.034-.46 0-.915.01-1.36.034.44-.572.895-1.096 1.345-1.565zM12 8.1c.74 0 1.477.034 2.202.093.406.582.802 1.203 1.183 1.86.372.64.71 1.29 1.018 1.946-.308.655-.646 1.31-1.013 1.95-.38.66-.773 1.288-1.18 1.87a25.64 25.64 0 0 1-4.412.005 26.64 26.64 0 0 1-1.183-1.86c-.372-.64-.71-1.29-1.018-1.946a25.17 25.17 0 0 1 1.013-1.954c.38-.66.773-1.286 1.18-1.868A25.245 25.245 0 0 1 12 8.098zm-3.635.254c-.24.377-.48.763-.704 1.16-.225.39-.435.782-.635 1.174-.265-.656-.49-1.31-.676-1.947.64-.15 1.315-.283 2.015-.386zm7.26 0c.695.103 1.365.23 2.006.387-.18.632-.405 1.282-.66 1.933a25.952 25.952 0 0 0-1.345-2.32zm3.063.675c.484.15.944.317 1.375.498 1.732.74 2.852 1.708 2.852 2.476-.005.768-1.125 1.74-2.857 2.475-.42.18-.88.342-1.355.493a23.966 23.966 0 0 0-1.1-2.98c.45-1.017.81-2.01 1.085-2.964zm-13.395.004c.278.96.645 1.957 1.1 2.98a23.142 23.142 0 0 0-1.086 2.964c-.484-.15-.944-.318-1.37-.5-1.732-.737-2.852-1.706-2.852-2.474 0-.768 1.12-1.742 2.852-2.476.42-.18.88-.342 1.356-.494zm11.678 4.28c.265.657.49 1.312.676 1.948-.64.157-1.316.29-2.016.39a25.819 25.819 0 0 0 1.341-2.338zm-9.945.02c.2.392.41.783.64 1.175.23.39.465.772.705 1.143a22.005 22.005 0 0 1-2.006-.386c.18-.63.406-1.282.66-1.933zM17.92 16.32c.112.493.2.968.254 1.423.23 1.868-.054 3.32-.714 3.708-.147.09-.338.128-.563.128-1.012 0-2.514-.807-4.11-2.28.686-.72 1.37-1.536 2.02-2.44 1.107-.118 2.154-.3 3.113-.54zm-11.83.01c.96.234 2.006.415 3.107.532.66.905 1.345 1.727 2.035 2.446-1.595 1.483-3.092 2.295-4.11 2.295a1.185 1.185 0 0 1-.553-.132c-.666-.38-.955-1.834-.73-3.703.054-.46.142-.944.25-1.438zm4.56.64c.44.02.89.034 1.345.034.46 0 .915-.01 1.36-.034-.44.572-.895 1.095-1.345 1.565-.455-.47-.91-.993-1.36-1.565z\"></path>{\" \"}\n    </svg>\n  ),\n);\nReactJs.displayName = \"ReactJs\";\n\nconst TailwindCss = forwardRef<SVGSVGElement, SVGProps<SVGSVGElement>>(\n  ({ className, ...props }, ref) => (\n    <svg\n      ref={ref}\n      xmlns=\"http://www.w3.org/2000/svg\"\n      {...props}\n      className={cn(className)}\n      fill=\"currentColor\"\n      viewBox=\"0 0 24 24\"\n    >\n      <path d=\"M12.001 4.8c-3.2 0-5.2 1.6-6 4.8 1.2-1.6 2.6-2.2 4.2-1.8.913.228 1.565.89 2.288 1.624C13.666 10.618 15.027 12 18.001 12c3.2 0 5.2-1.6 6-4.8-1.2 1.6-2.6 2.2-4.2 1.8-.913-.228-1.565-.89-2.288-1.624C16.337 6.182 14.976 4.8 12.001 4.8zm-6 7.2c-3.2 0-5.2 1.6-6 4.8 1.2-1.6 2.6-2.2 4.2-1.8.913.228 1.565.89 2.288 1.624 1.177 1.194 2.538 2.576 5.512 2.576 3.2 0 5.2-1.6 6-4.8-1.2 1.6-2.6 2.2-4.2 1.8-.913-.228-1.565-.89-2.288-1.624C10.337 13.382 8.976 12 6.001 12z\"></path>\n    </svg>\n  ),\n);\nTailwindCss.displayName = \"TailwindCss\";\n\nconst LuciaAuth = forwardRef<SVGSVGElement, SVGProps<SVGSVGElement>>(\n  ({ className, ...props }, ref) => (\n    <svg\n      ref={ref}\n      {...props}\n      viewBox=\"0 0 2000 2000\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      fill=\"currentColor\"\n      className={cn(className)}\n    >\n      <path d=\"m1647.66,1673.36L1000,72.73,352.34,1673.36l-102.74,253.91h1500.8l-102.74-253.91Zm-647.66-549l-442.82,545.39,99.55-246.04,343.27-848.35,343.26,848.35,99.55,246.04-442.81-545.39Z\" />\n    </svg>\n  ),\n);\nLuciaAuth.displayName = \"LuciaAuth\";\n\nconst Drizzle = forwardRef<SVGSVGElement, SVGProps<SVGSVGElement>>(\n  ({ className, ...props }, ref) => (\n    <svg\n      ref={ref}\n      {...props}\n      viewBox=\"0 0 160 160\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      fill=\"currentColor\"\n      className={cn(className)}\n    >\n      <rect\n        width=\"9.63139\"\n        height=\"40.8516\"\n        rx=\"4.8157\"\n        transform=\"matrix(0.873028 0.48767 -0.497212 0.867629 43.4805 67.3037)\"\n        fill=\"currentColor\"\n      ></rect>\n      <rect\n        width=\"9.63139\"\n        height=\"40.8516\"\n        rx=\"4.8157\"\n        transform=\"matrix(0.873028 0.48767 -0.497212 0.867629 76.9395 46.5342)\"\n        fill=\"currentColor\"\n      ></rect>\n      <rect\n        width=\"9.63139\"\n        height=\"40.8516\"\n        rx=\"4.8157\"\n        transform=\"matrix(0.873028 0.48767 -0.497212 0.867629 128.424 46.5352)\"\n        fill=\"currentColor\"\n      ></rect>\n      <rect\n        width=\"9.63139\"\n        height=\"40.8516\"\n        rx=\"4.8157\"\n        transform=\"matrix(0.873028 0.48767 -0.497212 0.867629 94.957 67.3037)\"\n        fill=\"currentColor\"\n      ></rect>\n    </svg>\n  ),\n);\nDrizzle.displayName = \"Drizzle\";\n\nconst TRPC = forwardRef<SVGSVGElement, SVGProps<SVGSVGElement>>(({ className, ...props }, ref) => (\n  <svg\n    ref={ref}\n    {...props}\n    viewBox=\"0 0 512 512\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    fill=\"currentColor\"\n    className={cn(className)}\n  >\n    <g>\n      <polygon points=\"246.2,162.3 202.9,137.3 202.9,187.4 246.2,212.4\" />\n      <polygon points=\"96.5,357.9 139.9,382.9 139.9,332.9 96.5,307.8\" />\n      <polygon points=\"149.1,266.8 105.7,291.9 149.1,316.9 192.4,291.9\" />\n      <polygon points=\"264.7,212.4 308,187.4 308,137.3 264.7,162.3\" />\n      <polygon points=\"298.8,121.3 255.4,96.3 212.2,121.3 255.4,146.4\" />\n      <polygon points=\"201.7,307.8 158.3,332.9 158.3,382.9 201.7,357.9\" />\n      <path\n        d=\"M362,0H150C67.2,0,0,67.2,0,150v212c0,82.8,67.2,150,150,150h212c82.8,0,150-67.2,150-150V150\n\t\tC512,67.2,444.8,0,362,0z M435.6,368.6l-71,41l-31.5-18.2l-76.7,44.3l-76.2-44l-31.1,18l-71-41.1v-82l22.2-12.8v-85.5l84.2-48.6\n\t\tl0,0V116l71-41l71.1,41v22.5l86,49.7v85l23.1,13.3V368.6z\"\n      />\n      <polygon points=\"373.8,383 417.2,357.9 417.2,307.8 373.8,332.9\" />\n      <polygon points=\"364.6,266.9 321.3,291.9 364.6,317 407.9,291.9\" />\n      <polygon\n        points=\"293.6,286.5 364.6,245.5 394.1,262.6 394.1,198.9 326.5,159.9 326.5,198 255.5,239 184.5,198 \n\t\t184.5,160.9 184.4,160.9 118.7,198.9 118.7,263.1 149.1,245.5 220.1,286.5 220.1,368.5 198.6,381 256.4,414.3 314.6,380.7 \n\t\t293.6,368.5\"\n      />\n      <polygon points=\"312,358 355.4,383 355.4,332.9 312,307.9\" />\n    </g>\n  </svg>\n));\nTRPC.displayName = \"TRPC\";\n\nconst ShadcnUi = forwardRef<SVGSVGElement, SVGProps<SVGSVGElement>>(\n  ({ className, ...props }, ref) => (\n    <svg\n      ref={ref}\n      {...props}\n      viewBox=\"0 0 256 256\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      fill=\"currentColor\"\n      className={cn(className)}\n    >\n      <rect width=\"256\" height=\"256\" fill=\"none\"></rect>\n      <line\n        x1=\"208\"\n        y1=\"128\"\n        x2=\"128\"\n        y2=\"208\"\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        strokeWidth=\"16\"\n      ></line>\n      <line\n        x1=\"192\"\n        y1=\"40\"\n        x2=\"40\"\n        y2=\"192\"\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        strokeWidth=\"16\"\n      ></line>\n    </svg>\n  ),\n);\nShadcnUi.displayName = \"ShadcnUi\";\n\nconst ReactEmail = forwardRef<SVGSVGElement, SVGProps<SVGSVGElement>>(\n  ({ className, ...props }, ref) => (\n    <svg\n      ref={ref}\n      {...props}\n      viewBox=\"0 0 32 32\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      className={cn(className)}\n    >\n      <g clipPath=\"url(#clip0_27_291)\">\n        <path\n          fillRule=\"evenodd\"\n          clipRule=\"evenodd\"\n          d=\"M24.4558 24.4853C25.2339 23.7073 25.3805 22.6549 25.2947 21.746C25.2078 20.8254 24.8697 19.8258 24.3896 18.8287C23.957 17.9302 23.3802 16.9745 22.6821 16C23.3802 15.0255 23.957 14.0698 24.3896 13.1713C24.8697 12.1742 25.2078 11.1746 25.2947 10.254C25.3805 9.34508 25.2339 8.29273 24.4558 7.51472C23.6778 6.73671 22.6255 6.59004 21.7165 6.67584C20.796 6.76273 19.7964 7.10086 18.7993 7.58094C17.9007 8.01357 16.945 8.59036 15.9706 9.28842C14.9961 8.59036 14.0404 8.01357 13.1418 7.58094C12.1447 7.10086 11.1451 6.76273 10.2246 6.67584C9.31564 6.59004 8.26329 6.73671 7.48528 7.51472C6.70727 8.29273 6.5606 9.34508 6.6464 10.254C6.7333 11.1746 7.07142 12.1742 7.5515 13.1713C7.98414 14.0698 8.56092 15.0255 9.25898 16C8.56092 16.9745 7.98414 17.9302 7.5515 18.8287C7.07142 19.8258 6.7333 20.8254 6.6464 21.746C6.5606 22.6549 6.70727 23.7073 7.48528 24.4853C8.26329 25.2633 9.31564 25.41 10.2246 25.3242C11.1451 25.2373 12.1447 24.8991 13.1418 24.4191C14.0404 23.9864 14.9961 23.4096 15.9706 22.7116C16.945 23.4096 17.9007 23.9864 18.7993 24.4191C19.7964 24.8991 20.796 25.2373 21.7165 25.3242C22.6255 25.41 23.6778 25.2633 24.4558 24.4853ZM15.9706 20.948C16.8399 20.2684 17.724 19.4874 18.591 18.6205C19.458 17.7535 20.239 16.8693 20.9186 16C20.239 15.1307 19.458 14.2465 18.591 13.3795C17.724 12.5126 16.8399 11.7316 15.9706 11.052C15.1012 11.7316 14.2171 12.5126 13.3501 13.3795C12.4831 14.2465 11.7021 15.1307 11.0225 16C11.7021 16.8693 12.4831 17.7535 13.3501 18.6205C14.2171 19.4874 15.1012 20.2684 15.9706 20.948ZM17.1498 21.8145C17.968 21.1558 18.7885 20.4195 19.5893 19.6187C20.39 18.818 21.1264 17.9974 21.7851 17.1792C23.7187 19.9919 24.4627 22.4819 23.4576 23.487C22.4524 24.4922 19.9625 23.7482 17.1498 21.8145ZM10.156 17.1792C10.8148 17.9974 11.5511 18.818 12.3518 19.6187C13.1526 20.4195 13.9731 21.1558 14.7914 21.8145C11.9786 23.7482 9.48871 24.4922 8.48355 23.487C7.47839 22.4819 8.22238 19.9919 10.156 17.1792ZM10.156 14.8208C10.8148 14.0026 11.5511 13.182 12.3518 12.3813C13.1526 11.5805 13.9731 10.8442 14.7914 10.1855C11.9786 8.25182 9.48871 7.50783 8.48355 8.51299C7.47839 9.51815 8.22238 12.0081 10.156 14.8208ZM17.1498 10.1855C17.968 10.8442 18.7885 11.5805 19.5893 12.3813C20.39 13.182 21.1264 14.0026 21.7851 14.8208C23.7187 12.0081 24.4627 9.51815 23.4576 8.51299C22.4524 7.50783 19.9625 8.25182 17.1498 10.1855Z\"\n          fill=\"currentColor\"\n          stroke=\"currentColor\"\n          strokeWidth=\"0.5\"\n        ></path>\n      </g>\n      <path\n        d=\"M36 22.176V13.744H37.936L37.968 16.432L37.696 15.824C37.8133 15.3973 38.016 15.0133 38.304 14.672C38.592 14.3307 38.9227 14.064 39.296 13.872C39.68 13.6693 40.08 13.568 40.496 13.568C40.6773 13.568 40.848 13.584 41.008 13.616C41.1787 13.648 41.3173 13.6853 41.424 13.728L40.896 15.888C40.7787 15.824 40.6347 15.7707 40.464 15.728C40.2933 15.6853 40.1227 15.664 39.952 15.664C39.6853 15.664 39.4293 15.7173 39.184 15.824C38.9493 15.92 38.7413 16.0587 38.56 16.24C38.3787 16.4213 38.2347 16.6347 38.128 16.88C38.032 17.1147 37.984 17.3813 37.984 17.68V22.176H36Z\"\n        fill=\"currentColor\"\n      ></path>\n      <path\n        d=\"M45.907 22.336C45.0217 22.336 44.2377 22.1493 43.555 21.776C42.883 21.4027 42.355 20.896 41.971 20.256C41.5977 19.6053 41.411 18.864 41.411 18.032C41.411 17.3707 41.5177 16.768 41.731 16.224C41.9443 15.68 42.2377 15.2107 42.611 14.816C42.995 14.4107 43.4483 14.1013 43.971 13.888C44.5043 13.664 45.0857 13.552 45.715 13.552C46.2697 13.552 46.787 13.6587 47.267 13.872C47.747 14.0853 48.163 14.3787 48.515 14.752C48.867 15.1147 49.1337 15.552 49.315 16.064C49.507 16.5653 49.5977 17.1147 49.587 17.712L49.571 18.4H42.739L42.371 17.056H47.923L47.667 17.328V16.976C47.635 16.6453 47.5283 16.3573 47.347 16.112C47.1657 15.856 46.931 15.6587 46.643 15.52C46.3657 15.3707 46.0563 15.296 45.715 15.296C45.1923 15.296 44.7497 15.3973 44.387 15.6C44.035 15.8027 43.7683 16.096 43.587 16.48C43.4057 16.8533 43.315 17.3227 43.315 17.888C43.315 18.432 43.427 18.9067 43.651 19.312C43.8857 19.7173 44.211 20.032 44.627 20.256C45.0537 20.4693 45.5497 20.576 46.115 20.576C46.5097 20.576 46.8723 20.512 47.203 20.384C47.5337 20.256 47.891 20.0267 48.275 19.696L49.251 21.056C48.963 21.3227 48.6323 21.552 48.259 21.744C47.8963 21.9253 47.5123 22.0693 47.107 22.176C46.7017 22.2827 46.3017 22.336 45.907 22.336Z\"\n        fill=\"currentColor\"\n      ></path>\n      <path\n        d=\"M54.094 22.336C53.4007 22.336 52.7713 22.144 52.206 21.76C51.6407 21.376 51.1873 20.8533 50.846 20.192C50.5047 19.5307 50.334 18.7787 50.334 17.936C50.334 17.0933 50.5047 16.3413 50.846 15.68C51.1873 15.0187 51.6513 14.5013 52.238 14.128C52.8247 13.7547 53.486 13.568 54.222 13.568C54.6487 13.568 55.038 13.632 55.39 13.76C55.742 13.8773 56.0513 14.048 56.318 14.272C56.5847 14.496 56.8033 14.752 56.974 15.04C57.1553 15.328 57.278 15.6373 57.342 15.968L56.91 15.856V13.744H58.894V22.176H56.894V20.16L57.358 20.08C57.2833 20.368 57.1447 20.6507 56.942 20.928C56.75 21.1947 56.5047 21.4347 56.206 21.648C55.918 21.8507 55.5927 22.016 55.23 22.144C54.878 22.272 54.4993 22.336 54.094 22.336ZM54.638 20.592C55.0967 20.592 55.502 20.48 55.854 20.256C56.206 20.032 56.478 19.7227 56.67 19.328C56.8727 18.9227 56.974 18.4587 56.974 17.936C56.974 17.424 56.8727 16.9707 56.67 16.576C56.478 16.1813 56.206 15.872 55.854 15.648C55.502 15.424 55.0967 15.312 54.638 15.312C54.1793 15.312 53.774 15.424 53.422 15.648C53.0807 15.872 52.814 16.1813 52.622 16.576C52.43 16.9707 52.334 17.424 52.334 17.936C52.334 18.4587 52.43 18.9227 52.622 19.328C52.814 19.7227 53.0807 20.032 53.422 20.256C53.774 20.48 54.1793 20.592 54.638 20.592Z\"\n        fill=\"currentColor\"\n      ></path>\n      <path\n        d=\"M64.3716 22.336C63.5823 22.336 62.873 22.144 62.2436 21.76C61.6143 21.376 61.1183 20.8533 60.7556 20.192C60.393 19.5307 60.2116 18.784 60.2116 17.952C60.2116 17.12 60.393 16.3733 60.7556 15.712C61.1183 15.0507 61.6143 14.528 62.2436 14.144C62.873 13.76 63.5823 13.568 64.3716 13.568C65.129 13.568 65.817 13.712 66.4356 14C67.0543 14.288 67.5343 14.688 67.8756 15.2L66.7876 16.512C66.6276 16.288 66.425 16.0853 66.1796 15.904C65.9343 15.7227 65.673 15.5787 65.3956 15.472C65.1183 15.3653 64.841 15.312 64.5636 15.312C64.0943 15.312 63.673 15.4293 63.2996 15.664C62.937 15.888 62.649 16.2027 62.4356 16.608C62.2223 17.0027 62.1156 17.4507 62.1156 17.952C62.1156 18.4533 62.2223 18.9013 62.4356 19.296C62.6596 19.6907 62.9583 20.0053 63.3316 20.24C63.705 20.4747 64.121 20.592 64.5796 20.592C64.857 20.592 65.1236 20.5493 65.3796 20.464C65.6463 20.368 65.897 20.2347 66.1316 20.064C66.3663 19.8933 66.585 19.68 66.7876 19.424L67.8756 20.752C67.513 21.2213 67.0116 21.6053 66.3716 21.904C65.7423 22.192 65.0756 22.336 64.3716 22.336Z\"\n        fill=\"currentColor\"\n      ></path>\n      <path\n        d=\"M69.8726 22.176V11.6H71.8406V22.176H69.8726ZM68.2086 15.568V13.744H73.6806V15.568H68.2086Z\"\n        fill=\"currentColor\"\n      ></path>\n      <path\n        d=\"M82.9945 22.336C82.1092 22.336 81.3252 22.1493 80.6425 21.776C79.9705 21.4027 79.4425 20.896 79.0585 20.256C78.6852 19.6053 78.4985 18.864 78.4985 18.032C78.4985 17.3707 78.6052 16.768 78.8185 16.224C79.0318 15.68 79.3252 15.2107 79.6985 14.816C80.0825 14.4107 80.5358 14.1013 81.0585 13.888C81.5918 13.664 82.1732 13.552 82.8025 13.552C83.3572 13.552 83.8745 13.6587 84.3545 13.872C84.8345 14.0853 85.2505 14.3787 85.6025 14.752C85.9545 15.1147 86.2212 15.552 86.4025 16.064C86.5945 16.5653 86.6852 17.1147 86.6745 17.712L86.6585 18.4H79.8265L79.4585 17.056H85.0105L84.7545 17.328V16.976C84.7225 16.6453 84.6158 16.3573 84.4345 16.112C84.2532 15.856 84.0185 15.6587 83.7305 15.52C83.4532 15.3707 83.1438 15.296 82.8025 15.296C82.2798 15.296 81.8372 15.3973 81.4745 15.6C81.1225 15.8027 80.8558 16.096 80.6745 16.48C80.4932 16.8533 80.4025 17.3227 80.4025 17.888C80.4025 18.432 80.5145 18.9067 80.7385 19.312C80.9732 19.7173 81.2985 20.032 81.7145 20.256C82.1412 20.4693 82.6372 20.576 83.2025 20.576C83.5972 20.576 83.9598 20.512 84.2905 20.384C84.6212 20.256 84.9785 20.0267 85.3625 19.696L86.3385 21.056C86.0505 21.3227 85.7198 21.552 85.3465 21.744C84.9838 21.9253 84.5998 22.0693 84.1945 22.176C83.7892 22.2827 83.3892 22.336 82.9945 22.336Z\"\n        fill=\"currentColor\"\n      ></path>\n      <path\n        d=\"M87.9655 22.176V13.744H89.9015L89.9335 15.44L89.6135 15.568C89.7095 15.2907 89.8535 15.0347 90.0455 14.8C90.2375 14.5547 90.4668 14.3467 90.7335 14.176C91.0002 13.9947 91.2828 13.856 91.5815 13.76C91.8802 13.6533 92.1842 13.6 92.4935 13.6C92.9522 13.6 93.3575 13.6747 93.7095 13.824C94.0722 13.9627 94.3708 14.1867 94.6055 14.496C94.8508 14.8053 95.0322 15.2 95.1495 15.68L94.8455 15.616L94.9735 15.36C95.0908 15.104 95.2562 14.8747 95.4695 14.672C95.6828 14.4587 95.9228 14.272 96.1895 14.112C96.4562 13.9413 96.7335 13.8133 97.0215 13.728C97.3202 13.6427 97.6135 13.6 97.9015 13.6C98.5415 13.6 99.0748 13.728 99.5015 13.984C99.9282 14.24 100.248 14.6293 100.462 15.152C100.675 15.6747 100.782 16.32 100.782 17.088V22.176H98.7975V17.216C98.7975 16.7893 98.7388 16.4373 98.6215 16.16C98.5148 15.8827 98.3442 15.68 98.1095 15.552C97.8855 15.4133 97.6028 15.344 97.2615 15.344C96.9948 15.344 96.7388 15.392 96.4935 15.488C96.2588 15.5733 96.0562 15.7013 95.8855 15.872C95.7148 16.032 95.5815 16.2187 95.4855 16.432C95.3895 16.6453 95.3415 16.88 95.3415 17.136V22.176H93.3575V17.2C93.3575 16.7947 93.2988 16.4587 93.1815 16.192C93.0642 15.9147 92.8935 15.7067 92.6695 15.568C92.4455 15.4187 92.1735 15.344 91.8535 15.344C91.5868 15.344 91.3362 15.392 91.1015 15.488C90.8668 15.5733 90.6642 15.696 90.4935 15.856C90.3228 16.016 90.1895 16.2027 90.0935 16.416C89.9975 16.6293 89.9495 16.864 89.9495 17.12V22.176H87.9655Z\"\n        fill=\"currentColor\"\n      ></path>\n      <path\n        d=\"M105.73 22.336C105.037 22.336 104.408 22.144 103.842 21.76C103.277 21.376 102.824 20.8533 102.482 20.192C102.141 19.5307 101.97 18.7787 101.97 17.936C101.97 17.0933 102.141 16.3413 102.482 15.68C102.824 15.0187 103.288 14.5013 103.874 14.128C104.461 13.7547 105.122 13.568 105.858 13.568C106.285 13.568 106.674 13.632 107.026 13.76C107.378 13.8773 107.688 14.048 107.954 14.272C108.221 14.496 108.44 14.752 108.61 15.04C108.792 15.328 108.914 15.6373 108.978 15.968L108.546 15.856V13.744H110.53V22.176H108.53V20.16L108.994 20.08C108.92 20.368 108.781 20.6507 108.578 20.928C108.386 21.1947 108.141 21.4347 107.842 21.648C107.554 21.8507 107.229 22.016 106.866 22.144C106.514 22.272 106.136 22.336 105.73 22.336ZM106.274 20.592C106.733 20.592 107.138 20.48 107.49 20.256C107.842 20.032 108.114 19.7227 108.306 19.328C108.509 18.9227 108.61 18.4587 108.61 17.936C108.61 17.424 108.509 16.9707 108.306 16.576C108.114 16.1813 107.842 15.872 107.49 15.648C107.138 15.424 106.733 15.312 106.274 15.312C105.816 15.312 105.41 15.424 105.058 15.648C104.717 15.872 104.45 16.1813 104.258 16.576C104.066 16.9707 103.97 17.424 103.97 17.936C103.97 18.4587 104.066 18.9227 104.258 19.328C104.45 19.7227 104.717 20.032 105.058 20.256C105.41 20.48 105.816 20.592 106.274 20.592Z\"\n        fill=\"currentColor\"\n      ></path>\n      <path\n        d=\"M112.616 22.176V13.744H114.584V22.176H112.616ZM113.576 11.952C113.181 11.952 112.872 11.856 112.648 11.664C112.435 11.4613 112.328 11.1787 112.328 10.816C112.328 10.4747 112.44 10.1973 112.664 9.984C112.888 9.77067 113.192 9.664 113.576 9.664C113.981 9.664 114.291 9.76534 114.504 9.968C114.728 10.16 114.84 10.4427 114.84 10.816C114.84 11.1467 114.728 11.4187 114.504 11.632C114.28 11.8453 113.971 11.952 113.576 11.952Z\"\n        fill=\"currentColor\"\n      ></path>\n      <path d=\"M116.675 22.176V10.336H118.659V22.176H116.675Z\" fill=\"currentColor\"></path>\n      <defs>\n        <clipPath id=\"clip0_27_291\">\n          <rect width=\"32\" height=\"32\" rx=\"8\" fill=\"currentColor\"></rect>\n        </clipPath>\n      </defs>\n    </svg>\n  ),\n);\nReactEmail.displayName = \"ReactEmail\";\n\nconst StripeLogo = forwardRef<SVGSVGElement, SVGProps<SVGSVGElement>>(\n  ({ className, ...props }, ref) => (\n    <svg\n      ref={ref}\n      {...props}\n      viewBox=\"0 0 384 512\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      fill=\"currentColor\"\n      className={cn(className)}\n    >\n      <path d=\"M155.3 154.6c0-22.3 18.6-30.9 48.4-30.9 43.4 0 98.5 13.3 141.9 36.7V26.1C298.3 7.2 251.1 0 203.8 0 88.1 0 11 60.4 11 161.4c0 157.9 216.8 132.3 216.8 200.4 0 26.4-22.9 34.9-54.7 34.9-47.2 0-108.2-19.5-156.1-45.5v128.5a396.1 396.1 0 0 0 156 32.4c118.6 0 200.3-51 200.3-153.6 0-170.2-218-139.7-218-203.9z\" />\n    </svg>\n  ),\n);\nStripeLogo.displayName = \"StripeLogo\";\n\nexport {\n  Drizzle,\n  LuciaAuth,\n  NextjsDark,\n  NextjsLight,\n  ReactEmail,\n  ReactJs,\n  ShadcnUi,\n  StripeLogo,\n  TailwindCss,\n  TRPC,\n};\n"
  },
  {
    "path": "src/app/(landing)/_components/footer.tsx",
    "content": "import { ThemeToggle } from \"@/components/theme-toggle\";\nimport { CodeIcon } from \"@radix-ui/react-icons\";\n\nconst githubUrl = \"https://github.com/iamtouha/next-lucia-auth\";\nconst twitterUrl = \"https://twitter.com/iamtouha\";\n\nexport const Footer = () => {\n  return (\n    <footer className=\"px-4 py-6\">\n      <div className=\"container flex items-center p-0\">\n        <CodeIcon className=\"mr-2 h-6 w-6\" />\n        <p className=\"text-sm\">\n          Built by{\" \"}\n          <a className=\"underline underline-offset-4\" href={twitterUrl}>\n            iamtouha\n          </a>\n          . Get the source code from{\" \"}\n          <a className=\"underline underline-offset-4\" href={githubUrl}>\n            GitHub\n          </a>\n          .\n        </p>\n        <div className=\"ml-auto\">\n          <ThemeToggle />\n        </div>\n      </div>\n    </footer>\n  );\n};\n"
  },
  {
    "path": "src/app/(landing)/_components/header.tsx",
    "content": "import Link from \"next/link\";\nimport { RocketIcon } from \"@/components/icons\";\nimport { APP_TITLE } from \"@/lib/constants\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport { HamburgerMenuIcon } from \"@radix-ui/react-icons\";\n\nconst routes = [\n  { name: \"Home\", href: \"/\" },\n  { name: \"Features\", href: \"/#features\" },\n  {\n    name: \"Documentation\",\n    href: \"https://www.touha.dev/posts/simple-nextjs-t3-authentication-with-lucia\",\n  },\n] as const;\n\nexport const Header = () => {\n  return (\n    <header className=\"px-2 py-4 lg:py-6\">\n      <div className=\"container flex items-center gap-2 p-0\">\n        <DropdownMenu>\n          <DropdownMenuTrigger asChild>\n            <Button\n              className=\"focus:outline-none focus:ring-1 md:hidden\"\n              size=\"icon\"\n              variant=\"outline\"\n            >\n              <HamburgerMenuIcon className=\"h-5 w-5\" />\n            </Button>\n          </DropdownMenuTrigger>\n          <DropdownMenuContent align=\"start\">\n            <div className=\"py-1\">\n              {routes.map(({ name, href }) => (\n                <DropdownMenuItem key={name} asChild>\n                  <Link href={href}>{name}</Link>\n                </DropdownMenuItem>\n              ))}\n            </div>\n          </DropdownMenuContent>\n        </DropdownMenu>\n        <Link\n          className=\"flex items-center justify-center text-xl font-medium\"\n          href=\"/\"\n        >\n          <RocketIcon className=\"mr-2 h-5 w-5\" /> {APP_TITLE}\n        </Link>\n        <nav className=\"ml-10 hidden gap-4 sm:gap-6 md:flex\">\n          {routes.map(({ name, href }) => (\n            <Link\n              key={name}\n              className=\"text-sm font-medium text-muted-foreground/70 transition-colors hover:text-muted-foreground\"\n              href={href}\n            >\n              {name}\n            </Link>\n          ))}\n        </nav>\n        <div className=\"ml-auto\">\n          <Button asChild variant={\"secondary\"}>\n            <Link href=\"/login\">Login</Link>\n          </Button>\n        </div>\n      </div>\n    </header>\n  );\n};\n"
  },
  {
    "path": "src/app/(landing)/_components/hover-card.tsx",
    "content": "\"use client\";\nimport { Card, CardDescription, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport React, { useRef, useState } from \"react\";\n\ntype FeaturesProps = {\n  name: string;\n  description: string;\n  logo: React.ReactNode;\n};\n\nconst CardSpotlight = (props: FeaturesProps) => {\n  const divRef = useRef<HTMLDivElement>(null);\n  const [isFocused, setIsFocused] = useState(false);\n  const [position, setPosition] = useState({ x: 0, y: 0 });\n  const [opacity, setOpacity] = useState(0);\n\n  const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {\n    if (!divRef.current || isFocused) return;\n\n    const div = divRef.current;\n    const rect = div.getBoundingClientRect();\n\n    setPosition({ x: e.clientX - rect.left, y: e.clientY - rect.top });\n  };\n\n  const handleFocus = () => {\n    setIsFocused(true);\n    setOpacity(1);\n  };\n\n  const handleBlur = () => {\n    setIsFocused(false);\n    setOpacity(0);\n  };\n\n  const handleMouseEnter = () => {\n    setOpacity(1);\n  };\n\n  const handleMouseLeave = () => {\n    setOpacity(0);\n  };\n\n  return (\n    <Card\n      ref={divRef}\n      onMouseMove={handleMouseMove}\n      onFocus={handleFocus}\n      onBlur={handleBlur}\n      onMouseEnter={handleMouseEnter}\n      onMouseLeave={handleMouseLeave}\n      className=\"relative overflow-hidden rounded-xl border bg-white dark:border-gray-800 dark:bg-gradient-to-r dark:from-black dark:to-neutral-950 dark:shadow-2xl\"\n    >\n      <div\n        className=\"pointer-events-none absolute -inset-px opacity-0 transition duration-300\"\n        style={{\n          opacity,\n          background: `radial-gradient(600px circle at ${position.x}px ${position.y}px, rgba(255,182,255,.1), transparent 40%)`,\n        }}\n      />\n      <div className=\"pl-6 pt-6\">{props.logo}</div>\n      <CardHeader className=\"pb-6\">\n        <CardTitle className=\"text-xl\">{props.name}</CardTitle>\n        <CardDescription>{props.description}</CardDescription>\n      </CardHeader>\n    </Card>\n  );\n};\n\nexport default CardSpotlight;\n"
  },
  {
    "path": "src/app/(landing)/layout.tsx",
    "content": "import { APP_TITLE } from \"@/lib/constants\";\nimport { type Metadata } from \"next\";\nimport { type ReactNode } from \"react\";\nimport { Footer } from \"./_components/footer\";\nimport { Header } from \"./_components/header\";\n\nexport const metadata: Metadata = {\n  title: APP_TITLE,\n  description: \"A Next.js starter with T3 stack and Lucia auth.\",\n};\n\nfunction LandingPageLayout({ children }: { children: ReactNode }) {\n  return (\n    <>\n      <Header />\n      {children}\n      <div className=\"h-20\"></div>\n      <Footer />\n    </>\n  );\n}\n\nexport default LandingPageLayout;\n"
  },
  {
    "path": "src/app/(landing)/page.tsx",
    "content": "import { PlusIcon } from \"@/components/icons\";\nimport { Button } from \"@/components/ui/button\";\nimport { GitHubLogoIcon } from \"@radix-ui/react-icons\";\nimport { type Metadata } from \"next\";\nimport Link from \"next/link\";\nimport { CopyToClipboard } from \"./_components/copy-to-clipboard\";\nimport {\n  Drizzle,\n  LuciaAuth,\n  NextjsDark,\n  NextjsLight,\n  ReactEmail,\n  ReactJs,\n  ShadcnUi,\n  StripeLogo,\n  TRPC,\n  TailwindCss,\n} from \"./_components/feature-icons\";\nimport CardSpotlight from \"./_components/hover-card\";\n\nexport const metadata: Metadata = {\n  title: \"Next.js Lucia Auth Starter Template\",\n  description:\n    \"A Next.js starter template with nextjs and Lucia auth. Includes drizzle, trpc, react-email, tailwindcss and shadcn-ui\",\n};\n\nconst githubUrl = \"https://github.com/iamtouha/next-lucia-auth\";\n\nconst features = [\n  {\n    name: \"Next.js\",\n    description: \"The React Framework for Production\",\n    logo: NextjsIcon,\n  },\n  {\n    name: \"React.js\",\n    description: \"Server and client components.\",\n    logo: ReactJs,\n  },\n  {\n    name: \"Authentication\",\n    description: \"Credential authentication with password reset and email validation\",\n    logo: LuciaAuth,\n  },\n  {\n    name: \"Database\",\n    description: \"Drizzle with postgres database\",\n    logo: Drizzle,\n  },\n  {\n    name: \"TypeSafe Backend\",\n    description: \"Preserve type safety from backend to frontend with tRPC\",\n    logo: TRPC,\n  },\n  {\n    name: \"Subscription\",\n    description: \"Subscription with stripe\",\n    logo: StripeLogo,\n  },\n  {\n    name: \"Tailwindcss\",\n    description: \"Simple and elegant UI components built with Tailwind CSS\",\n    logo: TailwindCss,\n  },\n  {\n    name: \"Shadcn UI\",\n    description: \"A set of beautifully designed UI components for React\",\n    logo: ShadcnUi,\n  },\n  {\n    name: \"React Email\",\n    description: \"Write emails in React with ease.\",\n    logo: ReactEmail,\n  },\n];\n\nconst HomePage = () => {\n  return (\n    <>\n      <section className=\"mx-auto grid min-h-[calc(100vh-300px)] max-w-5xl flex-col  items-center justify-center gap-4 py-10 text-center  md:py-12\">\n        <div className=\"p-4\">\n          <div className=\"mb-10 flex items-center justify-center gap-3\">\n            <NextjsIcon className=\"h-[52px] w-[52px]\" />\n            <PlusIcon className=\"h-8 w-8\" />\n            <LuciaAuth className=\"h-14 w-14\" />\n          </div>\n          <h1 className=\"text-balance bg-gradient-to-tr  from-black/70 via-black to-black/60 bg-clip-text text-center text-3xl font-bold text-transparent dark:from-zinc-400/10 dark:via-white/90 dark:to-white/20  sm:text-5xl md:text-6xl lg:text-7xl\">\n            Next.js Lucia Auth Starter Template\n          </h1>\n          <p className=\"mb-10 mt-4 text-balance text-center text-muted-foreground md:text-lg lg:text-xl\">\n            A Next.js Authentication starter template (password reset, email validation and oAuth).\n            Includes Lucia, Drizzle, tRPC, Stripe, tailwindcss, shadcn-ui and react-email.\n          </p>\n          <div className=\"mb-10\">\n            <div className=\"mx-auto max-w-[430px]\">\n              <CopyToClipboard text={\"git clone \" + githubUrl} />\n            </div>\n          </div>\n          <div className=\"flex justify-center gap-4\">\n            <Button size=\"lg\" variant=\"outline\" asChild>\n              <a href={githubUrl}>\n                <GitHubLogoIcon className=\"mr-1 h-5 w-5\" />\n                GitHub\n              </a>\n            </Button>\n            <Button size=\"lg\" asChild>\n              <Link href=\"/login\">Get Started</Link>\n            </Button>\n          </div>\n        </div>\n      </section>\n      <section>\n        <div className=\"container mx-auto lg:max-w-screen-lg\">\n          <h1 className=\"mb-4 text-center text-3xl font-bold md:text-4xl lg:text-5xl\">\n            <a id=\"features\"></a> Features\n          </h1>\n          <p className=\"mb-10 text-balance text-center text-muted-foreground md:text-lg lg:text-xl\">\n            This starter template is a guide to help you get started with Next.js for large scale\n            applications. Feel free to add or remove features to suit your needs.\n          </p>\n          <div className=\"grid gap-6 sm:grid-cols-2 md:grid-cols-3\">\n            {features.map((feature, i) => (\n              <CardSpotlight\n                key={i}\n                name={feature.name}\n                description={feature.description}\n                logo={<feature.logo className=\"h-12 w-12\" />}\n              />\n            ))}\n          </div>\n        </div>\n      </section>\n    </>\n  );\n};\n\nexport default HomePage;\n\nfunction NextjsIcon({ className }: { className?: string }) {\n  return (\n    <>\n      <NextjsLight className={className + \" dark:hidden\"} />\n      <NextjsDark className={className + \" hidden dark:block\"} />\n    </>\n  );\n}\n"
  },
  {
    "path": "src/app/(main)/_components/footer.tsx",
    "content": "import { ThemeToggle } from \"@/components/theme-toggle\";\nimport { CodeIcon } from \"@radix-ui/react-icons\";\n\nconst githubUrl = \"https://github.com/iamtouha/next-lucia-auth\";\nconst twitterUrl = \"https://twitter.com/iamtouha\";\n\nexport const Footer = () => {\n  return (\n    <footer className=\"mt-6 px-4 py-6\">\n      <div className=\"container flex items-center p-0\">\n        <CodeIcon className=\"mr-2 h-6 w-6\" />\n        <p className=\"text-sm\">\n          Built by{\" \"}\n          <a className=\"underline underline-offset-4\" href={twitterUrl}>\n            iamtouha\n          </a>\n          . Get the source code from{\" \"}\n          <a className=\"underline underline-offset-4\" href={githubUrl}>\n            GitHub\n          </a>\n          .\n        </p>\n        <div className=\"ml-auto\">\n          <ThemeToggle />\n        </div>\n      </div>\n    </footer>\n  );\n};\n"
  },
  {
    "path": "src/app/(main)/_components/header.tsx",
    "content": "import { UserDropdown } from \"@/app/(main)/_components/user-dropdown\";\nimport { RocketIcon } from \"@/components/icons\";\nimport { validateRequest } from \"@/lib/auth/validate-request\";\nimport { APP_TITLE } from \"@/lib/constants\";\nimport Link from \"next/link\";\n\nexport const Header = async () => {\n  const { user } = await validateRequest();\n\n  return (\n    <header className=\"sticky top-0 z-10 border-b bg-background/80 p-0\">\n      <div className=\"container flex items-center gap-2 px-2 py-2 lg:px-4\">\n        <Link className=\"flex items-center justify-center text-xl font-medium\" href=\"/\">\n          <RocketIcon className=\"mr-2 h-5 w-5\" /> {APP_TITLE} Dashboard\n        </Link>\n        {user ? <UserDropdown email={user.email} avatar={user.avatar} className=\"ml-auto\" /> : null}\n      </div>\n    </header>\n  );\n};\n"
  },
  {
    "path": "src/app/(main)/_components/user-dropdown.tsx",
    "content": "\"use client\";\n\nimport { ExclamationTriangleIcon } from \"@/components/icons\";\nimport { LoadingButton } from \"@/components/loading-button\";\nimport {\n  AlertDialog,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogHeader,\n  AlertDialogTitle,\n  AlertDialogTrigger,\n} from \"@/components/ui/alert-dialog\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuItem,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport { logout } from \"@/lib/auth/actions\";\nimport { APP_TITLE } from \"@/lib/constants\";\nimport Link from \"next/link\";\nimport { useState } from \"react\";\nimport { toast } from \"sonner\";\n\nexport const UserDropdown = ({\n  email,\n  avatar,\n  className,\n}: {\n  email: string;\n  avatar?: string | null;\n  className?: string;\n}) => {\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger className={className}>\n        {/* eslint @next/next/no-img-element:off */}\n        <img\n          src={avatar ?? \"https://source.boringavatars.com/marble/60/\" + email}\n          alt=\"Avatar\"\n          className=\"block h-8 w-8 rounded-full leading-none\"\n          width={64}\n          height={64}\n        ></img>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"end\">\n        <DropdownMenuLabel className=\"text-muted-foreground\">{email}</DropdownMenuLabel>\n        <DropdownMenuSeparator />\n        <DropdownMenuGroup>\n          <DropdownMenuItem className=\"cursor-pointer text-muted-foreground\" asChild>\n            <Link href=\"/dashboard\">Dashboard</Link>\n          </DropdownMenuItem>\n          <DropdownMenuItem className=\"cursor-pointer text-muted-foreground\" asChild>\n            <Link href=\"/dashboard/billing\">Billing</Link>\n          </DropdownMenuItem>\n          <DropdownMenuItem className=\"cursor-pointer text-muted-foreground\" asChild>\n            <Link href=\"/dashboard/settings\">Settings</Link>\n          </DropdownMenuItem>\n        </DropdownMenuGroup>\n        <DropdownMenuSeparator />\n\n        <DropdownMenuLabel className=\"p-0\">\n          <SignoutConfirmation />\n        </DropdownMenuLabel>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n};\n\nconst SignoutConfirmation = () => {\n  const [open, setOpen] = useState(false);\n  const [isLoading, setIsLoading] = useState(false);\n\n  const handleSignout = async () => {\n    setIsLoading(true);\n    try {\n      await logout();\n      toast(\"Signed out successfully\");\n    } catch (error) {\n      if (error instanceof Error) {\n        toast(error.message, {\n          icon: <ExclamationTriangleIcon className=\"h-4 w-4 text-destructive\" />,\n        });\n      }\n    } finally {\n      setOpen(false);\n      setIsLoading(false);\n    }\n  };\n\n  return (\n    <AlertDialog open={open} onOpenChange={setOpen}>\n      <AlertDialogTrigger\n        className=\"px-2 py-1.5 text-sm text-muted-foreground outline-none\"\n        asChild\n      >\n        <button>Sign out</button>\n      </AlertDialogTrigger>\n      <AlertDialogContent className=\"max-w-xs\">\n        <AlertDialogHeader>\n          <AlertDialogTitle className=\"text-center\">Sign out from {APP_TITLE}?</AlertDialogTitle>\n          <AlertDialogDescription>You will be redirected to the home page.</AlertDialogDescription>\n        </AlertDialogHeader>\n        <div className=\"flex flex-col-reverse gap-2 sm:flex-row sm:justify-center\">\n          <Button variant=\"outline\" onClick={() => setOpen(false)}>\n            Cancel\n          </Button>\n          <LoadingButton loading={isLoading} onClick={handleSignout}>\n            Continue\n          </LoadingButton>\n        </div>\n      </AlertDialogContent>\n    </AlertDialog>\n  );\n};\n"
  },
  {
    "path": "src/app/(main)/account/page.tsx",
    "content": "import { SubmitButton } from \"@/components/submit-button\";\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardFooter,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card\";\nimport { logout } from \"@/lib/auth/actions\";\nimport { validateRequest } from \"@/lib/auth/validate-request\";\nimport { Paths } from \"@/lib/constants\";\nimport { redirect } from \"next/navigation\";\n\nexport default async function AccountPage() {\n  const { user } = await validateRequest();\n  if (!user) redirect(Paths.Login);\n\n  return (\n    <main className=\"container mx-auto min-h-screen p-4\">\n      <Card className=\"max-w-sm\">\n        <CardHeader>\n          <CardTitle> {user.email}!</CardTitle>\n          <CardDescription>You've successfully logged in!</CardDescription>\n        </CardHeader>\n        <CardContent>This is a private page.</CardContent>\n        <CardFooter>\n          <form action={logout}>\n            <SubmitButton variant=\"outline\">Logout</SubmitButton>\n          </form>\n        </CardFooter>\n      </Card>\n    </main>\n  );\n}\n"
  },
  {
    "path": "src/app/(main)/dashboard/_components/dashboard-nav.tsx",
    "content": "\"use client\";\n\nimport { CreditCard, FileTextIcon, GearIcon } from \"@/components/icons\";\nimport Link from \"next/link\";\nimport { usePathname } from \"next/navigation\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst items = [\n  {\n    title: \"Posts\",\n    href: \"/dashboard\",\n    icon: FileTextIcon,\n  },\n\n  {\n    title: \"Billing\",\n    href: \"/dashboard/billing\",\n    icon: CreditCard,\n  },\n  {\n    title: \"Settings\",\n    href: \"/dashboard/settings\",\n    icon: GearIcon,\n  },\n];\n\ninterface Props {\n  className?: string;\n}\n\nexport function DashboardNav({ className }: Props) {\n  const path = usePathname();\n\n  return (\n    <nav className={cn(className)}>\n      {items.map((item) => (\n        <Link href={item.href} key={item.href}>\n          <span\n            className={cn(\n              \"group flex items-center rounded-md px-3 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground\",\n              path === item.href ? \"bg-accent\" : \"transparent\",\n            )}\n          >\n            <item.icon className=\"mr-2 h-4 w-4\" />\n            <span>{item.title}</span>\n          </span>\n        </Link>\n      ))}\n    </nav>\n  );\n}\n"
  },
  {
    "path": "src/app/(main)/dashboard/_components/new-post.tsx",
    "content": "\"use client\";\n\nimport { FilePlusIcon } from \"@/components/icons\";\nimport { Button } from \"@/components/ui/button\";\nimport { api } from \"@/trpc/react\";\nimport { type RouterOutputs } from \"@/trpc/shared\";\nimport { useRouter } from \"next/navigation\";\nimport * as React from \"react\";\nimport { toast } from \"sonner\";\ninterface NewPostProps {\n  isEligible: boolean;\n  setOptimisticPosts: (action: {\n    action: \"add\" | \"delete\" | \"update\";\n    post: RouterOutputs[\"post\"][\"myPosts\"][number];\n  }) => void;\n}\n\nexport const NewPost = ({ isEligible, setOptimisticPosts }: NewPostProps) => {\n  const router = useRouter();\n  const post = api.post.create.useMutation();\n  const [isCreatePending, startCreateTransaction] = React.useTransition();\n\n  const createPost = () => {\n    if (!isEligible) {\n      toast.message(\"You've reached the limit of posts for your current plan\", {\n        description: \"Upgrade to create more posts\",\n      });\n      return;\n    }\n\n    startCreateTransaction(async () => {\n      await post.mutateAsync(\n        {\n          title: \"Untitled Post\",\n          content: \"Write your content here\",\n          excerpt: \"untitled post\",\n        },\n        {\n          onSettled: () => {\n            setOptimisticPosts({\n              action: \"add\",\n              post: {\n                id: crypto.randomUUID(),\n                title: \"Untitled Post\",\n                excerpt: \"untitled post\",\n                status: \"draft\",\n                createdAt: new Date(),\n              },\n            });\n          },\n          onSuccess: ({ id }) => {\n            toast.success(\"Post created\");\n            router.refresh();\n            // This is a workaround for a bug in navigation because of router.refresh()\n            setTimeout(() => {\n              router.push(`/editor/${id}`);\n            }, 100);\n          },\n          onError: () => {\n            toast.error(\"Failed to create post\");\n          },\n        },\n      );\n    });\n  };\n\n  return (\n    <Button\n      onClick={createPost}\n      className=\"flex h-full cursor-pointer items-center justify-center bg-card p-6 text-muted-foreground transition-colors hover:bg-secondary/10 dark:border-none dark:bg-secondary/30 dark:hover:bg-secondary/50\"\n      disabled={isCreatePending}\n    >\n      <div className=\"flex flex-col items-center gap-4\">\n        <FilePlusIcon className=\"h-10 w-10\" />\n        <p className=\"text-sm\">New Post</p>\n      </div>\n    </Button>\n  );\n};\n"
  },
  {
    "path": "src/app/(main)/dashboard/_components/post-card-skeleton.tsx",
    "content": "import { Card, CardContent, CardFooter, CardHeader } from \"@/components/ui/card\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\n\nexport function PostCardSkeleton() {\n  return (\n    <Card>\n      <CardHeader>\n        <Skeleton className=\"h-7 w-24\" />\n        <Skeleton className=\"h-3 w-36\" />\n      </CardHeader>\n      <CardContent className=\"line-clamp-3 text-sm\">\n        <Skeleton className=\"h-5 w-24\" />\n      </CardContent>\n      <CardFooter className=\"justify-between gap-2\">\n        <Skeleton className=\"h-6 w-16\" />\n        <div className=\"flex items-center space-x-2\">\n          <Skeleton className=\"h-8 w-8\" />\n          <Skeleton className=\"h-8 w-12\" />\n        </div>\n      </CardFooter>\n    </Card>\n  );\n}\n"
  },
  {
    "path": "src/app/(main)/dashboard/_components/post-card.tsx",
    "content": "\"use client\";\n\nimport { Pencil2Icon, TrashIcon } from \"@/components/icons\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardFooter,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card\";\nimport { api } from \"@/trpc/react\";\nimport { type RouterOutputs } from \"@/trpc/shared\";\nimport Link from \"next/link\";\nimport { useRouter } from \"next/navigation\";\nimport * as React from \"react\";\nimport { toast } from \"sonner\";\n\ninterface PostCardProps {\n  post: RouterOutputs[\"post\"][\"myPosts\"][number];\n  userName?: string;\n  setOptimisticPosts: (action: {\n    action: \"add\" | \"delete\" | \"update\";\n    post: RouterOutputs[\"post\"][\"myPosts\"][number];\n  }) => void;\n}\n\nexport const PostCard = ({ post, userName, setOptimisticPosts }: PostCardProps) => {\n  const router = useRouter();\n  const postMutation = api.post.delete.useMutation();\n  const [isDeletePending, startDeleteTransition] = React.useTransition();\n\n  return (\n    <Card>\n      <CardHeader>\n        <CardTitle className=\"line-clamp-2 text-base\">{post.title}</CardTitle>\n        <CardDescription className=\"line-clamp-1 text-sm\">\n          {userName ? <span>{userName} at</span> : null}\n          {new Date(post.createdAt.toJSON()).toLocaleString(undefined, {\n            dateStyle: \"medium\",\n            timeStyle: \"short\",\n          })}\n        </CardDescription>\n      </CardHeader>\n      <CardContent className=\"line-clamp-3 text-sm\">{post.excerpt}</CardContent>\n      <CardFooter className=\"flex-row-reverse gap-2\">\n        <Button variant=\"secondary\" size=\"sm\" asChild>\n          <Link href={`/editor/${post.id}`}>\n            <Pencil2Icon className=\"mr-1 h-4 w-4\" />\n            <span>Edit</span>\n          </Link>\n        </Button>\n        <Button\n          variant=\"secondary\"\n          size=\"icon\"\n          className=\"h-8 w-8 text-destructive\"\n          onClick={() => {\n            startDeleteTransition(async () => {\n              await postMutation.mutateAsync(\n                { id: post.id },\n                {\n                  onSettled: () => {\n                    setOptimisticPosts({\n                      action: \"delete\",\n                      post,\n                    });\n                  },\n                  onSuccess: () => {\n                    toast.success(\"Post deleted\");\n                    router.refresh();\n                  },\n                  onError: () => {\n                    toast.error(\"Failed to delete post\");\n                  },\n                },\n              );\n            });\n          }}\n          disabled={isDeletePending}\n        >\n          <TrashIcon className=\"h-5 w-5\" />\n          <span className=\"sr-only\">Delete</span>\n        </Button>\n        <Badge variant=\"outline\" className=\"mr-auto rounded-lg capitalize\">\n          {post.status} Post\n        </Badge>\n      </CardFooter>\n    </Card>\n  );\n};\n"
  },
  {
    "path": "src/app/(main)/dashboard/_components/posts-skeleton.tsx",
    "content": "import { PostCardSkeleton } from \"./post-card-skeleton\";\n\nexport function PostsSkeleton() {\n  return (\n    <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-3\">\n      {Array.from({ length: 3 }).map((_, i) => (\n        <PostCardSkeleton key={i} />\n      ))}\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/app/(main)/dashboard/_components/posts.tsx",
    "content": "\"use client\";\n\nimport { type RouterOutputs } from \"@/trpc/shared\";\nimport * as React from \"react\";\nimport { NewPost } from \"./new-post\";\nimport { PostCard } from \"./post-card\";\n\ninterface PostsProps {\n  promises: Promise<[RouterOutputs[\"post\"][\"myPosts\"], RouterOutputs[\"stripe\"][\"getPlan\"]]>;\n}\n\nexport function Posts({ promises }: PostsProps) {\n  /**\n   * use is a React Hook that lets you read the value of a resource like a Promise or context.\n   * @see https://react.dev/reference/react/use\n   */\n  const [posts, subscriptionPlan] = React.use(promises);\n\n  /**\n   * useOptimistic is a React Hook that lets you show a different state while an async action is underway.\n   * It accepts some state as an argument and returns a copy of that state that can be different during the duration of an async action such as a network request.\n   * @see https://react.dev/reference/react/useOptimistic\n   */\n  const [optimisticPosts, setOptimisticPosts] = React.useOptimistic(\n    posts,\n    (\n      state,\n      {\n        action,\n        post,\n      }: {\n        action: \"add\" | \"delete\" | \"update\";\n        post: RouterOutputs[\"post\"][\"myPosts\"][number];\n      },\n    ) => {\n      switch (action) {\n        case \"delete\":\n          return state.filter((p) => p.id !== post.id);\n        case \"update\":\n          return state.map((p) => (p.id === post.id ? post : p));\n        default:\n          return [...state, post];\n      }\n    },\n  );\n\n  return (\n    <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-3\">\n      <NewPost\n        isEligible={(optimisticPosts.length < 2 || subscriptionPlan?.isPro) ?? false}\n        setOptimisticPosts={setOptimisticPosts}\n      />\n      {optimisticPosts.map((post) => (\n        <PostCard key={post.id} post={post} setOptimisticPosts={setOptimisticPosts} />\n      ))}\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/app/(main)/dashboard/_components/verificiation-warning.tsx",
    "content": "import { ExclamationTriangleIcon } from \"@/components/icons\";\n\nimport { Alert, AlertDescription, AlertTitle } from \"@/components/ui/alert\";\nimport { Button } from \"@/components/ui/button\";\nimport { validateRequest } from \"@/lib/auth/validate-request\";\nimport Link from \"next/link\";\n\nexport async function VerificiationWarning() {\n  const { user } = await validateRequest();\n\n  return user?.emailVerified === false ? (\n    <Alert className=\"rounded-lg bg-yellow-50 text-yellow-700 dark:bg-gray-800 dark:text-yellow-400\">\n      <ExclamationTriangleIcon className=\"h-5 w-5 !text-yellow-700 dark:!text-yellow-400\" />\n      <div className=\"flex lg:items-center\">\n        <div className=\"w-full\">\n          <AlertTitle>Account verification required</AlertTitle>\n          <AlertDescription>\n            A verification email has been sent to your email address. Please verify your account to\n            access all features.\n          </AlertDescription>\n        </div>\n        <Button size=\"sm\" asChild>\n          <Link href=\"/verify-email\">Verify Email</Link>\n        </Button>\n      </div>\n    </Alert>\n  ) : null;\n}\n"
  },
  {
    "path": "src/app/(main)/dashboard/billing/_components/billing-skeleton.tsx",
    "content": "import { Card, CardContent, CardFooter, CardHeader } from \"@/components/ui/card\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\n\nexport function BillingSkeleton() {\n  return (\n    <>\n      <section>\n        <Card className=\"space-y-2 p-8\">\n          <Skeleton className=\"h-7 w-24\" />\n          <Skeleton className=\"h-5 w-36\" />\n        </Card>\n      </section>\n      <section className=\"grid gap-6 lg:grid-cols-2\">\n        {Array.from({ length: 2 }).map((_, i) => (\n          <Card key={i} className=\"flex flex-col p-2\">\n            <CardHeader className=\"h-full\">\n              <Skeleton className=\"h-7 w-24\" />\n              <Skeleton className=\"h-4 w-36\" />\n            </CardHeader>\n            <CardContent className=\"h-full flex-1 space-y-6\">\n              <Skeleton className=\"h-8 w-24\" />\n              <div className=\"space-y-2\">\n                {Array.from({ length: 2 }).map((_, i) => (\n                  <div key={i} className=\"flex items-center gap-2\">\n                    <Skeleton className=\"h-5 w-5 rounded-full\" />\n                    <Skeleton className=\"h-4 w-24\" />\n                  </div>\n                ))}\n              </div>\n            </CardContent>\n            <CardFooter className=\"pt-4\">\n              <Skeleton className=\"h-10 w-full\" />\n            </CardFooter>\n          </Card>\n        ))}\n      </section>\n    </>\n  );\n}\n"
  },
  {
    "path": "src/app/(main)/dashboard/billing/_components/billing.tsx",
    "content": "import Link from \"next/link\";\n\nimport { CheckIcon } from \"@/components/icons\";\n\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardFooter,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card\";\nimport { formatDate } from \"@/lib/utils\";\nimport { type RouterOutputs } from \"@/trpc/shared\";\nimport { ManageSubscriptionForm } from \"./manage-subscription-form\";\n\ninterface BillingProps {\n  stripePromises: Promise<\n    [RouterOutputs[\"stripe\"][\"getPlans\"], RouterOutputs[\"stripe\"][\"getPlan\"]]\n  >;\n}\n\nexport async function Billing({ stripePromises }: BillingProps) {\n  const [plans, plan] = await stripePromises;\n\n  return (\n    <>\n      <section>\n        <Card className=\"space-y-2 p-8\">\n          <h3 className=\"text-lg font-semibold sm:text-xl\">{plan?.name ?? \"Free\"} plan</h3>\n          <p className=\"text-sm text-muted-foreground\">\n            {!plan?.isPro\n              ? \"The free plan is limited to 2 posts. Upgrade to the Pro plan to unlock unlimited posts.\"\n              : plan.isCanceled\n                ? \"Your plan will be canceled on \"\n                : \"Your plan renews on \"}\n            {plan?.stripeCurrentPeriodEnd ? formatDate(plan.stripeCurrentPeriodEnd) : null}\n          </p>\n        </Card>\n      </section>\n      <section className=\"grid gap-6 lg:grid-cols-2\">\n        {plans.map((item) => (\n          <Card key={item.name} className=\"flex flex-col p-2\">\n            <CardHeader className=\"h-full\">\n              <CardTitle className=\"line-clamp-1\">{item.name}</CardTitle>\n              <CardDescription className=\"line-clamp-2\">{item.description}</CardDescription>\n            </CardHeader>\n            <CardContent className=\"h-full flex-1 space-y-6\">\n              <div className=\"text-3xl font-bold\">\n                {item.price}\n                <span className=\"text-sm font-normal text-muted-foreground\">/month</span>\n              </div>\n              <div className=\"space-y-2\">\n                {item.features.map((feature) => (\n                  <div key={feature} className=\"flex items-center gap-2\">\n                    <div className=\"aspect-square shrink-0 rounded-full bg-foreground p-px text-background\">\n                      <CheckIcon className=\"size-4\" aria-hidden=\"true\" />\n                    </div>\n                    <span className=\"text-sm text-muted-foreground\">{feature}</span>\n                  </div>\n                ))}\n              </div>\n            </CardContent>\n            <CardFooter className=\"pt-4\">\n              {item.name === \"Free\" ? (\n                <Button className=\"w-full\" asChild>\n                  <Link href=\"/dashboard\">\n                    Get started\n                    <span className=\"sr-only\">Get started</span>\n                  </Link>\n                </Button>\n              ) : (\n                <ManageSubscriptionForm\n                  stripePriceId={item.stripePriceId}\n                  isPro={plan?.isPro ?? false}\n                  stripeCustomerId={plan?.stripeCustomerId}\n                  stripeSubscriptionId={plan?.stripeSubscriptionId}\n                />\n              )}\n            </CardFooter>\n          </Card>\n        ))}\n      </section>\n    </>\n  );\n}\n"
  },
  {
    "path": "src/app/(main)/dashboard/billing/_components/manage-subscription-form.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport type { ManageSubscriptionInput } from \"@/server/api/routers/stripe/stripe.input\";\nimport { api } from \"@/trpc/react\";\nimport { toast } from \"sonner\";\n\nexport function ManageSubscriptionForm({\n  isPro,\n  stripeCustomerId,\n  stripeSubscriptionId,\n  stripePriceId,\n}: ManageSubscriptionInput) {\n  const [isPending, startTransition] = React.useTransition();\n  const managePlanMutation = api.stripe.managePlan.useMutation();\n\n  function onSubmit(e: React.FormEvent<HTMLFormElement>) {\n    e.preventDefault();\n\n    startTransition(async () => {\n      try {\n        const session = await managePlanMutation.mutateAsync({\n          isPro,\n          stripeCustomerId,\n          stripeSubscriptionId,\n          stripePriceId,\n        });\n\n        if (session) {\n          window.location.href = session.url ?? \"/dashboard/billing\";\n        }\n      } catch (err) {\n        err instanceof Error\n          ? toast.error(err.message)\n          : toast.error(\"An error occurred. Please try again.\");\n      }\n    });\n  }\n\n  return (\n    <form className=\"w-full\" onSubmit={onSubmit}>\n      <Button className=\"w-full\" disabled={isPending}>\n        {isPending ? \"Loading...\" : isPro ? \"Manage plan\" : \"Subscribe now\"}\n      </Button>\n    </form>\n  );\n}\n"
  },
  {
    "path": "src/app/(main)/dashboard/billing/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport { redirect } from \"next/navigation\";\n\nimport { ExclamationTriangleIcon } from \"@/components/icons\";\n\nimport { Alert, AlertDescription, AlertTitle } from \"@/components/ui/alert\";\nimport { env } from \"@/env\";\nimport { validateRequest } from \"@/lib/auth/validate-request\";\nimport { APP_TITLE } from \"@/lib/constants\";\nimport { api } from \"@/trpc/server\";\nimport * as React from \"react\";\nimport { Billing } from \"./_components/billing\";\nimport { BillingSkeleton } from \"./_components/billing-skeleton\";\n\nexport const metadata: Metadata = {\n  metadataBase: new URL(env.NEXT_PUBLIC_APP_URL),\n  title: \"Billing\",\n  description: \"Manage your billing and subscription\",\n};\n\nexport default async function BillingPage() {\n  const { user } = await validateRequest();\n\n  if (!user) {\n    redirect(\"/signin\");\n  }\n\n  const stripePromises = Promise.all([api.stripe.getPlans.query(), api.stripe.getPlan.query()]);\n\n  return (\n    <div className=\"grid gap-8\">\n      <div>\n        <h1 className=\"text-3xl font-bold md:text-4xl\">Billing</h1>\n        <p className=\"text-sm text-muted-foreground\">Manage your billing and subscription</p>\n      </div>\n      <section>\n        <Alert className=\"p-6 [&>svg]:left-6 [&>svg]:top-6 [&>svg~*]:pl-10\">\n          <ExclamationTriangleIcon className=\"h-6 w-6\" />\n          <AlertTitle>This is a demo app.</AlertTitle>\n          <AlertDescription>\n            {APP_TITLE} app is a demo app using a Stripe test environment. You can find a list of\n            test card numbers on the{\" \"}\n            <a\n              href=\"https://stripe.com/docs/testing#cards\"\n              target=\"_blank\"\n              rel=\"noreferrer\"\n              className=\"font-medium underline underline-offset-4\"\n            >\n              Stripe docs\n            </a>\n            .\n          </AlertDescription>\n        </Alert>\n      </section>\n      <React.Suspense fallback={<BillingSkeleton />}>\n        <Billing stripePromises={stripePromises} />\n      </React.Suspense>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/app/(main)/dashboard/layout.tsx",
    "content": "import { DashboardNav } from \"./_components/dashboard-nav\";\nimport { VerificiationWarning } from \"./_components/verificiation-warning\";\n\ninterface Props {\n  children: React.ReactNode;\n}\n\nexport default function DashboardLayout({ children }: Props) {\n  return (\n    <div className=\"container min-h-[calc(100vh-180px)] px-2 pt-6 md:px-4\">\n      <div className=\"flex flex-col gap-6 md:flex-row lg:gap-10\">\n        <DashboardNav className=\"flex flex-shrink-0 gap-2 md:w-48 md:flex-col lg:w-80\" />\n        <main className=\"w-full space-y-4\">\n          <VerificiationWarning />\n          <div>{children}</div>\n        </main>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/app/(main)/dashboard/page.tsx",
    "content": "import { env } from \"@/env\";\nimport { validateRequest } from \"@/lib/auth/validate-request\";\nimport { Paths } from \"@/lib/constants\";\nimport { myPostsSchema } from \"@/server/api/routers/post/post.input\";\nimport { api } from \"@/trpc/server\";\nimport { type Metadata } from \"next\";\nimport { redirect } from \"next/navigation\";\nimport * as React from \"react\";\nimport { Posts } from \"./_components/posts\";\nimport { PostsSkeleton } from \"./_components/posts-skeleton\";\n\nexport const metadata: Metadata = {\n  metadataBase: new URL(env.NEXT_PUBLIC_APP_URL),\n  title: \"Posts\",\n  description: \"Manage your posts here\",\n};\n\ninterface Props {\n  searchParams: Record<string, string | string[] | undefined>;\n}\n\nexport default async function DashboardPage({ searchParams }: Props) {\n  const { page, perPage } = myPostsSchema.parse(searchParams);\n\n  const { user } = await validateRequest();\n  if (!user) redirect(Paths.Login);\n\n  /**\n   * Passing multiple promises to `Promise.all` to fetch data in parallel to prevent waterfall requests.\n   * Passing promises to the `Posts` component to make them hot promises (they can run without being awaited) to prevent waterfall requests.\n   * @see https://www.youtube.com/shorts/A7GGjutZxrs\n   * @see https://nextjs.org/docs/app/building-your-application/data-fetching/patterns#parallel-data-fetching\n   */\n  const promises = Promise.all([\n    api.post.myPosts.query({ page, perPage }),\n    api.stripe.getPlan.query(),\n  ]);\n\n  return (\n    <div>\n      <div className=\"mb-6\">\n        <h1 className=\"text-3xl font-bold md:text-4xl\">Posts</h1>\n        <p className=\"text-sm text-muted-foreground\">Manage your posts here</p>\n      </div>\n      <React.Suspense fallback={<PostsSkeleton />}>\n        <Posts promises={promises} />\n      </React.Suspense>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/app/(main)/dashboard/settings/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport { redirect } from \"next/navigation\";\n\nimport { env } from \"@/env\";\nimport { validateRequest } from \"@/lib/auth/validate-request\";\n\nexport const metadata: Metadata = {\n  metadataBase: new URL(env.NEXT_PUBLIC_APP_URL),\n  title: \"Billing\",\n  description: \"Manage your billing and subscription\",\n};\n\nexport default async function BillingPage() {\n  const { user } = await validateRequest();\n\n  if (!user) {\n    redirect(\"/signin\");\n  }\n\n  return (\n    <div className=\"grid gap-8\">\n      <div>\n        <h1 className=\"text-3xl font-bold md:text-4xl\">Settings</h1>\n        <p className=\"text-sm text-muted-foreground\">Manage your account settings</p>\n      </div>\n      <p>Work in progress...</p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/app/(main)/editor/[postId]/_components/post-editor.tsx",
    "content": "\"use client\";\nimport { useRef } from \"react\";\nimport { type RouterOutputs } from \"@/trpc/shared\";\nimport { useForm } from \"react-hook-form\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport {\n  Form,\n  FormControl,\n  FormDescription,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@/components/ui/form\";\nimport { Input } from \"@/components/ui/input\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { PostPreview } from \"./post-preview\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@/components/ui/tabs\";\nimport { api } from \"@/trpc/react\";\nimport { Pencil2Icon } from \"@/components/icons\";\nimport { LoadingButton } from \"@/components/loading-button\";\nimport Link from \"next/link\";\nimport { createPostSchema } from \"@/server/api/routers/post/post.input\";\n\nconst markdownlink = \"https://remarkjs.github.io/react-markdown/\";\n\ninterface Props {\n  post: RouterOutputs[\"post\"][\"get\"];\n}\n\nexport const PostEditor = ({ post }: Props) => {\n  if (!post) return null;\n  const formRef = useRef<HTMLFormElement>(null);\n  const updatePost = api.post.update.useMutation();\n  const form = useForm({\n    defaultValues: {\n      title: post.title,\n      excerpt: post.excerpt,\n      content: post.content,\n    },\n    resolver: zodResolver(createPostSchema),\n  });\n  const onSubmit = form.handleSubmit(async (values) => {\n    updatePost.mutate({ id: post.id, ...values });\n  });\n\n  return (\n    <>\n      <div className=\"flex items-center gap-2\">\n        <Pencil2Icon className=\"h-5 w-5\" />\n        <h1 className=\"text-2xl font-bold\">{post.title}</h1>\n\n        <LoadingButton\n          disabled={!form.formState.isDirty}\n          loading={updatePost.isLoading}\n          onClick={() => formRef.current?.requestSubmit()}\n          className=\"ml-auto\"\n        >\n          Save\n        </LoadingButton>\n      </div>\n      <div className=\"h-6\"></div>\n      <Form {...form}>\n        <form ref={formRef} onSubmit={onSubmit} className=\"block max-w-screen-md space-y-4\">\n          <FormField\n            control={form.control}\n            name=\"title\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Post Title</FormLabel>\n                <FormControl>\n                  <Input {...field} />\n                </FormControl>\n\n                <FormMessage />\n              </FormItem>\n            )}\n          />\n          <FormField\n            control={form.control}\n            name=\"excerpt\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Excerpt</FormLabel>\n                <FormControl>\n                  <Textarea {...field} rows={2} className=\"min-h-0\" />\n                </FormControl>\n                <FormDescription>A short description of your post</FormDescription>\n                <FormMessage />\n              </FormItem>\n            )}\n          />\n          <FormField\n            control={form.control}\n            name=\"content\"\n            render={({ field }) => (\n              <Tabs defaultValue=\"code\">\n                <TabsList>\n                  <TabsTrigger value=\"code\">Code</TabsTrigger>\n                  <TabsTrigger value=\"preview\">Preview</TabsTrigger>\n                </TabsList>\n                <TabsContent value=\"code\">\n                  <FormItem>\n                    <FormControl>\n                      <Textarea {...field} className=\"min-h-[200px]\" />\n                    </FormControl>\n                    <FormMessage />\n                  </FormItem>\n                </TabsContent>\n                <TabsContent value=\"preview\" className=\"space-y-2\">\n                  <div className=\"prose prose-sm min-h-[200px] max-w-[none] rounded-lg border px-3 py-2 dark:prose-invert\">\n                    <PostPreview text={form.watch(\"content\") || post.content} />\n                  </div>\n                </TabsContent>\n                <Link href={markdownlink}>\n                  <span className=\"text-[0.8rem] text-muted-foreground underline underline-offset-4\">\n                    Supports markdown\n                  </span>\n                </Link>\n              </Tabs>\n            )}\n          />\n        </form>\n      </Form>\n    </>\n  );\n};\n"
  },
  {
    "path": "src/app/(main)/editor/[postId]/_components/post-preview.tsx",
    "content": "import Markdown, { type Components } from \"react-markdown\";\nimport remarkGfm from \"remark-gfm\";\nimport rehypeRaw from \"rehype-raw\";\nimport { Prism as SyntaxHighlighter } from \"react-syntax-highlighter\";\nimport { materialOceanic } from \"react-syntax-highlighter/dist/cjs/styles/prism\";\n\nconst options: Components = {\n  code: (props) => (\n    <SyntaxHighlighter\n      language={props.className?.replace(/(?:lang(?:uage)?-)/, \"\")}\n      style={materialOceanic}\n      wrapLines={true}\n      className=\"not-prose rounded-md\"\n    >\n      {String(props.children)}\n    </SyntaxHighlighter>\n  ),\n};\n\nexport const PostPreview = ({ text }: { text: string }) => {\n  return (\n    <Markdown\n      remarkPlugins={[remarkGfm]}\n      rehypePlugins={[rehypeRaw]}\n      components={options}\n    >\n      {text}\n    </Markdown>\n  );\n};\n"
  },
  {
    "path": "src/app/(main)/editor/[postId]/page.tsx",
    "content": "import React from \"react\";\nimport { api } from \"@/trpc/server\";\nimport { notFound, redirect } from \"next/navigation\";\nimport { PostEditor } from \"./_components/post-editor\";\nimport { ArrowLeftIcon } from \"@/components/icons\";\nimport Link from \"next/link\";\nimport { validateRequest } from \"@/lib/auth/validate-request\";\nimport { Paths } from \"@/lib/constants\";\n\ninterface Props {\n  params: {\n    postId: string;\n  };\n}\n\nexport default async function EditPostPage({ params }: Props) {\n  const { user } = await validateRequest();\n  if (!user) redirect(Paths.Login);\n\n  const post = await api.post.get.query({ id: params.postId });\n  if (!post) notFound();\n\n  return (\n    <main className=\"container min-h-[calc(100vh-160px)] pt-3 md:max-w-screen-md\">\n      <Link\n        href=\"/dashboard\"\n        className=\"mb-3 flex items-center gap-2 text-sm text-muted-foreground hover:underline\"\n      >\n        <ArrowLeftIcon className=\"h-5 w-5\" /> back to dashboard\n      </Link>\n\n      <PostEditor post={post} />\n    </main>\n  );\n}\n"
  },
  {
    "path": "src/app/(main)/layout.tsx",
    "content": "import { type ReactNode } from \"react\";\nimport { Header } from \"./_components/header\";\nimport { Footer } from \"./_components/footer\";\n\nconst MainLayout = ({ children }: { children: ReactNode }) => {\n  return (\n    <>\n      <Header />\n      {children}\n      <Footer />\n    </>\n  );\n};\n\nexport default MainLayout;\n"
  },
  {
    "path": "src/app/api/trpc/[trpc]/route.ts",
    "content": "import { fetchRequestHandler } from \"@trpc/server/adapters/fetch\";\nimport { type NextRequest } from \"next/server\";\n\nimport { env } from \"@/env\";\nimport { appRouter } from \"@/server/api/root\";\nimport { createTRPCContext } from \"@/server/api/trpc\";\n\n/**\n * This wraps the `createTRPCContext` helper and provides the required context for the tRPC API when\n * handling a HTTP request (e.g. when you make requests from Client Components).\n */\nconst createContext = async (req: NextRequest) => {\n  return createTRPCContext({ headers: req.headers });\n};\n\nconst handler = (req: NextRequest) =>\n  fetchRequestHandler({\n    endpoint: \"/api/trpc\",\n    req,\n    router: appRouter,\n    createContext: () => createContext(req),\n    onError:\n      env.NODE_ENV === \"development\"\n        ? ({ path, error }) => {\n            console.error(\n              `❌ tRPC failed on ${path ?? \"<no-path>\"}: ${error.message}`,\n            );\n          }\n        : undefined,\n  });\n\nexport { handler as GET, handler as POST };\n"
  },
  {
    "path": "src/app/api/webhooks/stripe/route.ts",
    "content": "import { headers } from \"next/headers\";\n\nimport type Stripe from \"stripe\";\n\nimport { env } from \"@/env\";\nimport { stripe } from \"@/lib/stripe\";\nimport { db } from \"@/server/db\";\nimport { users } from \"@/server/db/schema\";\nimport { eq } from \"drizzle-orm\";\n\nexport async function POST(req: Request) {\n  const body = await req.text();\n  const signature = headers().get(\"Stripe-Signature\") ?? \"\";\n\n  let event: Stripe.Event;\n\n  try {\n    event = stripe.webhooks.constructEvent(\n      body,\n      signature,\n      env.STRIPE_WEBHOOK_SECRET,\n    );\n  } catch (err) {\n    return new Response(\n      `Webhook Error: ${err instanceof Error ? err.message : \"Unknown error.\"}`,\n      { status: 400 },\n    );\n  }\n\n  switch (event.type) {\n    case \"checkout.session.completed\": {\n      const checkoutSessionCompleted = event.data.object;\n\n      const userId = checkoutSessionCompleted?.metadata?.userId;\n\n      if (!userId) {\n        return new Response(\"User id not found in checkout session metadata.\", {\n          status: 404,\n        });\n      }\n\n      // Retrieve the subscription details from Stripe\n      const subscription = await stripe.subscriptions.retrieve(\n        checkoutSessionCompleted.subscription as string,\n      );\n\n      // Update the user stripe into in our database\n      // Since this is the initial subscription, we need to update\n      // the subscription id and customer id\n      await db\n        .update(users)\n        .set({\n          stripeSubscriptionId: subscription.id,\n          stripeCustomerId: subscription.customer as string,\n          stripePriceId: subscription.items.data[0]?.price.id,\n          stripeCurrentPeriodEnd: new Date(\n            subscription.current_period_end * 1000,\n          ),\n        })\n        .where(eq(users.id, userId));\n\n      break;\n    }\n    case \"invoice.payment_succeeded\": {\n      const invoicePaymentSucceeded = event.data.object;\n\n      const userId = invoicePaymentSucceeded?.metadata?.userId;\n\n      if (!userId) {\n        return new Response(\"User id not found in invoice metadata.\", {\n          status: 404,\n        });\n      }\n\n      // Retrieve the subscription details from Stripe\n      const subscription = await stripe.subscriptions.retrieve(\n        invoicePaymentSucceeded.subscription as string,\n      );\n\n      // Update the price id and set the new period end\n      await db\n        .update(users)\n        .set({\n          stripePriceId: subscription.items.data[0]?.price.id,\n          stripeCurrentPeriodEnd: new Date(\n            subscription.current_period_end * 1000,\n          ),\n        })\n        .where(eq(users.id, userId));\n\n      break;\n    }\n    default:\n      console.warn(`Unhandled event type: ${event.type}`);\n  }\n\n  return new Response(null, { status: 200 });\n}\n"
  },
  {
    "path": "src/app/icon.tsx",
    "content": "import { ImageResponse } from \"next/og\";\n\n// Route segment config\nexport const runtime = \"edge\";\n\n// Image metadata\nexport const size = {\n  width: 32,\n  height: 32,\n};\nexport const contentType = \"image/png\";\n\n// Image generation\nexport default function Icon() {\n  return new ImageResponse(\n    (\n      // ImageResponse JSX element\n      <div\n        tw=\"flex items-center justify-center bg-black text-[24px] leading-8 text-white\"\n        style={{\n          width: 32,\n          height: 32,\n        }}\n      >\n        <svg\n          width=\"24\"\n          height=\"24\"\n          viewBox=\"0 0 15 15\"\n          fill=\"none\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n        >\n          <path\n            d=\"M6.85357 3.85355L7.65355 3.05353C8.2981 2.40901 9.42858 1.96172 10.552 1.80125C11.1056 1.72217 11.6291 1.71725 12.0564 1.78124C12.4987 1.84748 12.7698 1.97696 12.8965 2.10357C13.0231 2.23018 13.1526 2.50125 13.2188 2.94357C13.2828 3.37086 13.2779 3.89439 13.1988 4.44801C13.0383 5.57139 12.591 6.70188 11.9464 7.34645L7.49999 11.7929L6.35354 10.6465C6.15827 10.4512 5.84169 10.4512 5.64643 10.6465C5.45117 10.8417 5.45117 11.1583 5.64643 11.3536L7.14644 12.8536C7.34171 13.0488 7.65829 13.0488 7.85355 12.8536L8.40073 12.3064L9.57124 14.2572C9.65046 14.3893 9.78608 14.4774 9.9389 14.4963C10.0917 14.5151 10.2447 14.4624 10.3535 14.3536L12.3535 12.3536C12.4648 12.2423 12.5172 12.0851 12.495 11.9293L12.0303 8.67679L12.6536 8.05355C13.509 7.19808 14.0117 5.82855 14.1887 4.58943C14.2784 3.9618 14.2891 3.33847 14.2078 2.79546C14.1287 2.26748 13.9519 1.74482 13.6035 1.39645C13.2552 1.04809 12.7325 0.871332 12.2045 0.792264C11.6615 0.710945 11.0382 0.721644 10.4105 0.8113C9.17143 0.988306 7.80189 1.491 6.94644 2.34642L6.32322 2.96968L3.07071 2.50504C2.91492 2.48278 2.75773 2.53517 2.64645 2.64646L0.646451 4.64645C0.537579 4.75533 0.484938 4.90829 0.50375 5.0611C0.522563 5.21391 0.61073 5.34954 0.742757 5.42876L2.69364 6.59928L2.14646 7.14645C2.0527 7.24022 2.00002 7.3674 2.00002 7.50001C2.00002 7.63261 2.0527 7.75979 2.14646 7.85356L3.64647 9.35356C3.84173 9.54883 4.15831 9.54883 4.35357 9.35356C4.54884 9.1583 4.54884 8.84172 4.35357 8.64646L3.20712 7.50001L3.85357 6.85356L6.85357 3.85355ZM10.0993 13.1936L9.12959 11.5775L11.1464 9.56067L11.4697 11.8232L10.0993 13.1936ZM3.42251 5.87041L5.43935 3.85356L3.17678 3.53034L1.80638 4.90074L3.42251 5.87041ZM2.35356 10.3535C2.54882 10.1583 2.54882 9.8417 2.35356 9.64644C2.1583 9.45118 1.84171 9.45118 1.64645 9.64644L0.646451 10.6464C0.451188 10.8417 0.451188 11.1583 0.646451 11.3535C0.841713 11.5488 1.1583 11.5488 1.35356 11.3535L2.35356 10.3535ZM3.85358 11.8536C4.04884 11.6583 4.04885 11.3417 3.85359 11.1465C3.65833 10.9512 3.34175 10.9512 3.14648 11.1465L1.14645 13.1464C0.95119 13.3417 0.951187 13.6583 1.14645 13.8535C1.34171 14.0488 1.65829 14.0488 1.85355 13.8536L3.85358 11.8536ZM5.35356 13.3535C5.54882 13.1583 5.54882 12.8417 5.35356 12.6464C5.1583 12.4512 4.84171 12.4512 4.64645 12.6464L3.64645 13.6464C3.45119 13.8417 3.45119 14.1583 3.64645 14.3535C3.84171 14.5488 4.1583 14.5488 4.35356 14.3535L5.35356 13.3535ZM9.49997 6.74881C10.1897 6.74881 10.7488 6.1897 10.7488 5.5C10.7488 4.8103 10.1897 4.25118 9.49997 4.25118C8.81026 4.25118 8.25115 4.8103 8.25115 5.5C8.25115 6.1897 8.81026 6.74881 9.49997 6.74881Z\"\n            fill=\"currentColor\"\n            fill-rule=\"evenodd\"\n            clip-rule=\"evenodd\"\n          ></path>\n        </svg>\n      </div>\n    ),\n    // ImageResponse options\n    {\n      // For convenience, we can re-use the exported icons size metadata\n      // config to also set the ImageResponse's width and height.\n      ...size,\n    },\n  );\n}\n"
  },
  {
    "path": "src/app/layout.tsx",
    "content": "import \"@/styles/globals.css\";\n\nimport { ThemeProvider } from \"@/components/theme-provider\";\nimport { Toaster } from \"@/components/ui/sonner\";\nimport { APP_TITLE } from \"@/lib/constants\";\nimport { fontSans } from \"@/lib/fonts\";\nimport { cn } from \"@/lib/utils\";\nimport { TRPCReactProvider } from \"@/trpc/react\";\nimport type { Metadata, Viewport } from \"next\";\n\nexport const metadata: Metadata = {\n  title: {\n    default: APP_TITLE,\n    template: `%s | ${APP_TITLE}`,\n  },\n  description: \"Acme - Simple auth with lucia and trpc\",\n  icons: [{ rel: \"icon\", url: \"/icon.png\" }],\n};\n\nexport const viewport: Viewport = {\n  themeColor: [\n    { media: \"(prefers-color-scheme: light)\", color: \"white\" },\n    { media: \"(prefers-color-scheme: dark)\", color: \"black\" },\n  ],\n};\n\nexport default function RootLayout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  return (\n    <html lang=\"en\" suppressHydrationWarning>\n      <body\n        className={cn(\n          \"min-h-screen bg-background font-sans antialiased\",\n          fontSans.variable,\n        )}\n      >\n        <ThemeProvider\n          attribute=\"class\"\n          defaultTheme=\"system\"\n          enableSystem\n          disableTransitionOnChange\n        >\n          <TRPCReactProvider>{children}</TRPCReactProvider>\n          <Toaster />\n        </ThemeProvider>\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "src/app/robots.ts",
    "content": "import { type MetadataRoute } from \"next\"\n\nimport { absoluteUrl } from \"@/lib/utils\"\n\nexport default function robots(): MetadataRoute.Robots {\n  return {\n    rules: {\n      userAgent: \"*\",\n      allow: \"/\",\n    },\n    sitemap: absoluteUrl(\"/sitemap.xml\"),\n  }\n}\n"
  },
  {
    "path": "src/app/sitemap.ts",
    "content": "import { type MetadataRoute } from \"next\";\n\nimport { absoluteUrl } from \"@/lib/utils\";\n\nexport default async function sitemap(): Promise<MetadataRoute.Sitemap> {\n  const routes = [\"\", \"/dashboard\", \"/dashboard/billing\"].map((route) => ({\n    url: absoluteUrl(route),\n    lastModified: new Date().toISOString(),\n  }));\n\n  return [...routes];\n}\n"
  },
  {
    "path": "src/components/icons.tsx",
    "content": "import { forwardRef, type SVGProps } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nconst AnimatedSpinner = forwardRef<SVGSVGElement, SVGProps<SVGSVGElement>>(\n  ({ className, ...props }, ref) => (\n    <svg\n      ref={ref}\n      {...props}\n      viewBox=\"0 0 24 24\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      fill=\"currentColor\"\n      className={cn(className)}\n    >\n      <g className=\"animated-spinner\">\n        <rect x=\"11\" y=\"1\" width=\"2\" height=\"5\" opacity=\".14\" />\n        <rect\n          x=\"11\"\n          y=\"1\"\n          width=\"2\"\n          height=\"5\"\n          transform=\"rotate(30 12 12)\"\n          opacity=\".29\"\n        />\n        <rect\n          x=\"11\"\n          y=\"1\"\n          width=\"2\"\n          height=\"5\"\n          transform=\"rotate(60 12 12)\"\n          opacity=\".43\"\n        />\n        <rect\n          x=\"11\"\n          y=\"1\"\n          width=\"2\"\n          height=\"5\"\n          transform=\"rotate(90 12 12)\"\n          opacity=\".57\"\n        />\n        <rect\n          x=\"11\"\n          y=\"1\"\n          width=\"2\"\n          height=\"5\"\n          transform=\"rotate(120 12 12)\"\n          opacity=\".71\"\n        />\n        <rect\n          x=\"11\"\n          y=\"1\"\n          width=\"2\"\n          height=\"5\"\n          transform=\"rotate(150 12 12)\"\n          opacity=\".86\"\n        />\n        <rect x=\"11\" y=\"1\" width=\"2\" height=\"5\" transform=\"rotate(180 12 12)\" />\n      </g>\n    </svg>\n  ),\n);\nAnimatedSpinner.displayName = \"AnimatedSpinner\";\n\nconst CreditCard = forwardRef<SVGSVGElement, SVGProps<SVGSVGElement>>(\n  ({ className, ...props }, ref) => (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      ref={ref}\n      {...props}\n      viewBox=\"0 0 24 24\"\n      className={cn(className)}\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth=\"2\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n    >\n      <rect x=\"2\" y=\"5\" width=\"20\" height=\"14\" rx=\"2\"></rect>\n      <line x1=\"2\" y1=\"10\" x2=\"22\" y2=\"10\"></line>\n    </svg>\n  ),\n);\nCreditCard.displayName = \"CreditCard\";\n\nexport { AnimatedSpinner, CreditCard };\n\nexport {\n  EyeOpenIcon,\n  EyeNoneIcon as EyeCloseIcon,\n  SunIcon,\n  MoonIcon,\n  ExclamationTriangleIcon,\n  ExitIcon,\n  EnterIcon,\n  GearIcon,\n  RocketIcon,\n  PlusIcon,\n  HamburgerMenuIcon,\n  Pencil2Icon,\n  UpdateIcon,\n  CheckCircledIcon,\n  PlayIcon,\n  TrashIcon,\n  ArchiveIcon,\n  ResetIcon,\n  DiscordLogoIcon,\n  FileTextIcon,\n  IdCardIcon,\n  PlusCircledIcon,\n  FilePlusIcon,\n  CheckIcon,\n  ChevronLeftIcon,\n  ChevronRightIcon,\n  DotsHorizontalIcon,\n  ArrowLeftIcon,\n} from \"@radix-ui/react-icons\";\n"
  },
  {
    "path": "src/components/loading-button.tsx",
    "content": "\"use client\";\n\nimport { forwardRef } from \"react\";\nimport { AnimatedSpinner } from \"@/components/icons\";\nimport { Button, type ButtonProps } from \"@/components/ui/button\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport interface LoadingButtonProps extends ButtonProps {\n  loading?: boolean;\n}\n\nconst LoadingButton = forwardRef<HTMLButtonElement, LoadingButtonProps>(\n  ({ loading = false, className, children, ...props }, ref) => {\n    return (\n      <Button\n        ref={ref}\n        {...props}\n        disabled={props.disabled ? props.disabled : loading}\n        className={cn(className, \"relative\")}\n      >\n        <span className={cn(loading ? \"opacity-0\" : \"\")}>{children}</span>\n        {loading ? (\n          <div className=\"absolute inset-0 grid place-items-center\">\n            <AnimatedSpinner className=\"h-6 w-6\" />\n          </div>\n        ) : null}\n      </Button>\n    );\n  },\n);\n\nLoadingButton.displayName = \"LoadingButton\";\n\nexport { LoadingButton };\n"
  },
  {
    "path": "src/components/password-input.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { EyeOpenIcon, EyeCloseIcon } from \"@/components/icons\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input, type InputProps } from \"@/components/ui/input\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst PasswordInputComponent = React.forwardRef<HTMLInputElement, InputProps>(\n  ({ className, ...props }, ref) => {\n    const [showPassword, setShowPassword] = React.useState(false);\n\n    return (\n      <div className=\"relative\">\n        <Input\n          type={showPassword ? \"text\" : \"password\"}\n          className={cn(\"pr-10\", className)}\n          ref={ref}\n          {...props}\n        />\n        <Button\n          type=\"button\"\n          variant=\"ghost\"\n          size=\"sm\"\n          className=\"absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent\"\n          onClick={() => setShowPassword((prev) => !prev)}\n          disabled={props.value === \"\" || props.disabled}\n        >\n          {showPassword ? (\n            <EyeCloseIcon className=\"h-4 w-4\" aria-hidden=\"true\" />\n          ) : (\n            <EyeOpenIcon className=\"h-4 w-4\" aria-hidden=\"true\" />\n          )}\n          <span className=\"sr-only\">\n            {showPassword ? \"Hide password\" : \"Show password\"}\n          </span>\n        </Button>\n      </div>\n    );\n  },\n);\nPasswordInputComponent.displayName = \"PasswordInput\";\n\nexport const PasswordInput = PasswordInputComponent;\n"
  },
  {
    "path": "src/components/responsive-dialog.tsx",
    "content": "\"use client\";\n\nimport {\n  useState,\n  type ReactNode,\n  type Dispatch,\n  type SetStateAction,\n} from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Dialog,\n  DialogTitle,\n  DialogContent,\n  DialogDescription,\n  DialogHeader,\n  DialogTrigger,\n  DialogFooter,\n} from \"@/components/ui/dialog\";\nimport {\n  Drawer,\n  DrawerClose,\n  DrawerContent,\n  DrawerDescription,\n  DrawerFooter,\n  DrawerHeader,\n  DrawerTitle,\n  DrawerTrigger,\n} from \"@/components/ui/drawer\";\nimport { useMediaQuery } from \"@/lib/hooks/use-media-query\";\nimport { cn } from \"@/lib/utils\";\n\ntype StatefulContent = ({\n  open,\n  setOpen,\n}: {\n  open: boolean;\n  setOpen: Dispatch<SetStateAction<boolean>>;\n}) => ReactNode | ReactNode[];\n\nexport const ResponsiveDialog = (props: {\n  trigger: ReactNode;\n  title?: ReactNode;\n  description?: ReactNode;\n  children: ReactNode | ReactNode[] | StatefulContent;\n  footer?: ReactNode;\n  contentClassName?: string;\n}) => {\n  const [open, setOpen] = useState(false);\n  const isDesktop = useMediaQuery(\"(min-width: 640px)\");\n\n  return isDesktop ? (\n    <Dialog open={open} onOpenChange={setOpen}>\n      <DialogTrigger asChild>{props.trigger}</DialogTrigger>\n      <DialogContent className={cn(\"max-w-md\", props.contentClassName)}>\n        <DialogHeader>\n          <DialogTitle>{props.title}</DialogTitle>\n          <DialogDescription>{props.description}</DialogDescription>\n        </DialogHeader>\n        {isFunctionType(props.children)\n          ? props.children({ open, setOpen })\n          : props.children}\n      </DialogContent>\n      {props.footer ? <DialogFooter>{props.footer}</DialogFooter> : null}\n    </Dialog>\n  ) : (\n    <Drawer open={open} onOpenChange={setOpen}>\n      <DrawerTrigger asChild>{props.trigger}</DrawerTrigger>\n      <DrawerContent>\n        <DrawerHeader className=\"text-left\">\n          <DrawerTitle>{props.title}</DrawerTitle>\n          <DrawerDescription>{props.description}</DrawerDescription>\n        </DrawerHeader>\n        <div className={cn(\"px-4\", props.contentClassName)}>\n          {isFunctionType(props.children)\n            ? props.children({ open, setOpen })\n            : props.children}\n        </div>\n        <DrawerFooter className=\"pt-2\">\n          {props.footer ? (\n            props.footer\n          ) : (\n            <DrawerClose asChild>\n              <Button variant=\"outline\">Cancel</Button>\n            </DrawerClose>\n          )}\n        </DrawerFooter>\n      </DrawerContent>\n    </Drawer>\n  );\n};\n\nconst isFunctionType = (\n  prop: ReactNode | ReactNode[] | StatefulContent,\n): prop is ({\n  open,\n  setOpen,\n}: {\n  open: boolean;\n  setOpen: Dispatch<SetStateAction<boolean>>;\n}) => ReactNode | ReactNode[] => {\n  return typeof prop === \"function\";\n};\n"
  },
  {
    "path": "src/components/submit-button.tsx",
    "content": "\"use client\";\n\nimport { forwardRef } from \"react\";\nimport { useFormStatus } from \"react-dom\";\nimport { LoadingButton } from \"@/components/loading-button\";\nimport type { ButtonProps } from \"@/components/ui/button\";\n\nconst SubmitButton = forwardRef<HTMLButtonElement, ButtonProps>(\n  ({ className, children, ...props }, ref) => {\n    const { pending } = useFormStatus();\n    return (\n      <LoadingButton\n        ref={ref}\n        {...props}\n        loading={pending}\n        className={className}\n      >\n        {children}\n      </LoadingButton>\n    );\n  },\n);\nSubmitButton.displayName = \"SubmitButton\";\n\nexport { SubmitButton };\n"
  },
  {
    "path": "src/components/theme-provider.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { ThemeProvider as NextThemesProvider } from \"next-themes\";\nimport { type ThemeProviderProps } from \"next-themes/dist/types\";\n\nexport function ThemeProvider({ children, ...props }: ThemeProviderProps) {\n  return <NextThemesProvider {...props}>{children}</NextThemesProvider>;\n}\n"
  },
  {
    "path": "src/components/theme-toggle.tsx",
    "content": "\"use client\";\n\nimport { useTheme } from \"next-themes\";\nimport { SunIcon, MoonIcon } from \"@/components/icons\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\n\nexport const ThemeToggle = () => {\n  const { setTheme } = useTheme();\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <Button variant=\"ghost\" size=\"icon\">\n          <SunIcon className=\"h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0\" />\n          <MoonIcon className=\"absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100\" />\n          <span className=\"sr-only\">Toggle theme</span>\n        </Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"end\">\n        <DropdownMenuItem onClick={() => setTheme(\"light\")}>\n          Light\n        </DropdownMenuItem>\n        <DropdownMenuItem onClick={() => setTheme(\"dark\")}>\n          Dark\n        </DropdownMenuItem>\n        <DropdownMenuItem onClick={() => setTheme(\"system\")}>\n          System\n        </DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n};\n"
  },
  {
    "path": "src/components/ui/alert-dialog.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as AlertDialogPrimitive from \"@radix-ui/react-alert-dialog\"\n\nimport { cn } from \"@/lib/utils\"\nimport { buttonVariants } from \"@/components/ui/button\"\n\nconst AlertDialog = AlertDialogPrimitive.Root\n\nconst AlertDialogTrigger = AlertDialogPrimitive.Trigger\n\nconst AlertDialogPortal = AlertDialogPrimitive.Portal\n\nconst AlertDialogOverlay = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPrimitive.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))\nAlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName\n\nconst AlertDialogContent = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPortal>\n    <AlertDialogOverlay />\n    <AlertDialogPrimitive.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  </AlertDialogPortal>\n))\nAlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName\n\nconst AlertDialogHeader = ({\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)\nAlertDialogHeader.displayName = \"AlertDialogHeader\"\n\nconst AlertDialogFooter = ({\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)\nAlertDialogFooter.displayName = \"AlertDialogFooter\"\n\nconst AlertDialogTitle = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPrimitive.Title\n    ref={ref}\n    className={cn(\"text-lg font-semibold\", className)}\n    {...props}\n  />\n))\nAlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName\n\nconst AlertDialogDescription = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPrimitive.Description\n    ref={ref}\n    className={cn(\"text-sm text-muted-foreground\", className)}\n    {...props}\n  />\n))\nAlertDialogDescription.displayName =\n  AlertDialogPrimitive.Description.displayName\n\nconst AlertDialogAction = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Action>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPrimitive.Action\n    ref={ref}\n    className={cn(buttonVariants(), className)}\n    {...props}\n  />\n))\nAlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName\n\nconst AlertDialogCancel = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Cancel>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPrimitive.Cancel\n    ref={ref}\n    className={cn(\n      buttonVariants({ variant: \"outline\" }),\n      \"mt-2 sm:mt-0\",\n      className\n    )}\n    {...props}\n  />\n))\nAlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName\n\nexport {\n  AlertDialog,\n  AlertDialogPortal,\n  AlertDialogOverlay,\n  AlertDialogTrigger,\n  AlertDialogContent,\n  AlertDialogHeader,\n  AlertDialogFooter,\n  AlertDialogTitle,\n  AlertDialogDescription,\n  AlertDialogAction,\n  AlertDialogCancel,\n}\n"
  },
  {
    "path": "src/components/ui/alert.tsx",
    "content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst alertVariants = cva(\n  \"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-background text-foreground\",\n        destructive:\n          \"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  }\n)\n\nconst Alert = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>\n>(({ className, variant, ...props }, ref) => (\n  <div\n    ref={ref}\n    role=\"alert\"\n    className={cn(alertVariants({ variant }), className)}\n    {...props}\n  />\n))\nAlert.displayName = \"Alert\"\n\nconst AlertTitle = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLHeadingElement>\n>(({ className, ...props }, ref) => (\n  <h5\n    ref={ref}\n    className={cn(\"mb-1 font-medium leading-none tracking-tight\", className)}\n    {...props}\n  />\n))\nAlertTitle.displayName = \"AlertTitle\"\n\nconst AlertDescription = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\"text-sm [&_p]:leading-relaxed\", className)}\n    {...props}\n  />\n))\nAlertDescription.displayName = \"AlertDescription\"\n\nexport { Alert, AlertTitle, AlertDescription }\n"
  },
  {
    "path": "src/components/ui/badge.tsx",
    "content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst badgeVariants = cva(\n  \"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2\",\n  {\n    variants: {\n      variant: {\n        default:\n          \"border-transparent bg-primary text-primary-foreground hover:bg-primary/80\",\n        secondary:\n          \"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n        destructive:\n          \"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80\",\n        outline: \"text-foreground\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  }\n)\n\nexport interface BadgeProps\n  extends React.HTMLAttributes<HTMLDivElement>,\n    VariantProps<typeof badgeVariants> {}\n\nfunction Badge({ className, variant, ...props }: BadgeProps) {\n  return (\n    <div className={cn(badgeVariants({ variant }), className)} {...props} />\n  )\n}\n\nexport { Badge, badgeVariants }\n"
  },
  {
    "path": "src/components/ui/button.tsx",
    "content": "import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\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-transparent 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: \"h-9 w-9\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  }\n)\n\nexport interface ButtonProps\n  extends React.ButtonHTMLAttributes<HTMLButtonElement>,\n    VariantProps<typeof buttonVariants> {\n  asChild?: boolean\n}\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n  ({ className, variant, size, asChild = false, ...props }, ref) => {\n    const Comp = asChild ? Slot : \"button\"\n    return (\n      <Comp\n        className={cn(buttonVariants({ variant, size, className }))}\n        ref={ref}\n        {...props}\n      />\n    )\n  }\n)\nButton.displayName = \"Button\"\n\nexport { Button, buttonVariants }\n"
  },
  {
    "path": "src/components/ui/card.tsx",
    "content": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Card = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\n      \"rounded-lg border bg-card text-card-foreground shadow-sm\",\n      className\n    )}\n    {...props}\n  />\n))\nCard.displayName = \"Card\"\n\nconst CardHeader = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\"flex flex-col space-y-1.5 p-6\", className)}\n    {...props}\n  />\n))\nCardHeader.displayName = \"CardHeader\"\n\nconst CardTitle = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLHeadingElement>\n>(({ className, ...props }, ref) => (\n  <h3\n    ref={ref}\n    className={cn(\n      \"text-2xl font-semibold leading-none tracking-tight\",\n      className\n    )}\n    {...props}\n  />\n))\nCardTitle.displayName = \"CardTitle\"\n\nconst CardDescription = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, ...props }, ref) => (\n  <p\n    ref={ref}\n    className={cn(\"text-sm text-muted-foreground\", className)}\n    {...props}\n  />\n))\nCardDescription.displayName = \"CardDescription\"\n\nconst CardContent = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div ref={ref} className={cn(\"p-6 pt-0\", className)} {...props} />\n))\nCardContent.displayName = \"CardContent\"\n\nconst CardFooter = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\"flex items-center p-6 pt-0\", className)}\n    {...props}\n  />\n))\nCardFooter.displayName = \"CardFooter\"\n\nexport { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }\n"
  },
  {
    "path": "src/components/ui/dialog.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\"\nimport { Cross1Icon } from \"@radix-ui/react-icons\";\n\nimport { cn } from \"@/lib/utils\"\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        <Cross1Icon className=\"h-4 w-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  DialogClose,\n  DialogTrigger,\n  DialogContent,\n  DialogHeader,\n  DialogFooter,\n  DialogTitle,\n  DialogDescription,\n}\n"
  },
  {
    "path": "src/components/ui/drawer.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Drawer as DrawerPrimitive } from \"vaul\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Drawer = ({\n  shouldScaleBackground = true,\n  ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (\n  <DrawerPrimitive.Root\n    shouldScaleBackground={shouldScaleBackground}\n    {...props}\n  />\n)\nDrawer.displayName = \"Drawer\"\n\nconst DrawerTrigger = DrawerPrimitive.Trigger\n\nconst DrawerPortal = DrawerPrimitive.Portal\n\nconst DrawerClose = DrawerPrimitive.Close\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    ref={ref}\n    className={cn(\"fixed inset-0 z-50 bg-black/80\", className)}\n    {...props}\n  />\n))\nDrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName\n\nconst DrawerContent = React.forwardRef<\n  React.ElementRef<typeof DrawerPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n  <DrawerPortal>\n    <DrawerOverlay />\n    <DrawerPrimitive.Content\n      ref={ref}\n      className={cn(\n        \"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background\",\n        className\n      )}\n      {...props}\n    >\n      <div className=\"mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted\" />\n      {children}\n    </DrawerPrimitive.Content>\n  </DrawerPortal>\n))\nDrawerContent.displayName = \"DrawerContent\"\n\nconst DrawerHeader = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\"grid gap-1.5 p-4 text-center sm:text-left\", className)}\n    {...props}\n  />\n)\nDrawerHeader.displayName = \"DrawerHeader\"\n\nconst DrawerFooter = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\"mt-auto flex flex-col gap-2 p-4\", className)}\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(\n      \"text-lg font-semibold leading-none tracking-tight\",\n      className\n    )}\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": "src/components/ui/dropdown-menu.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\"\nimport {\n  CheckIcon,\n  ChevronRightIcon,\n  DotFilledIcon,\n} from \"@radix-ui/react-icons\"\n\nimport { cn } from \"@/lib/utils\"\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 h-4 w-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 h-3.5 w-3.5 items-center justify-center\">\n      <DropdownMenuPrimitive.ItemIndicator>\n        <CheckIcon className=\"h-4 w-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 h-3.5 w-3.5 items-center justify-center\">\n      <DropdownMenuPrimitive.ItemIndicator>\n        <DotFilledIcon className=\"h-4 w-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": "src/components/ui/form.tsx",
    "content": "import * as React from \"react\";\nimport type * as LabelPrimitive from \"@radix-ui/react-label\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport {\n  Controller,\n  type ControllerProps,\n  type FieldPath,\n  type FieldValues,\n  FormProvider,\n  useFormContext,\n} from \"react-hook-form\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Label } from \"@/components/ui/label\";\n\nconst Form = FormProvider;\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 = 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\nexport {\n  useFormField,\n  Form,\n  FormItem,\n  FormLabel,\n  FormControl,\n  FormDescription,\n  FormMessage,\n  FormField,\n};\n"
  },
  {
    "path": "src/components/ui/input.tsx",
    "content": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport interface InputProps\n  extends React.InputHTMLAttributes<HTMLInputElement> {}\n\nconst Input = React.forwardRef<HTMLInputElement, InputProps>(\n  ({ className, type, ...props }, ref) => {\n    return (\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        {...props}\n      />\n    )\n  }\n)\nInput.displayName = \"Input\"\n\nexport { Input }\n"
  },
  {
    "path": "src/components/ui/label.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as LabelPrimitive from \"@radix-ui/react-label\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\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": "src/components/ui/pagination.tsx",
    "content": "import * as React from \"react\";\nimport Link from \"next/link\";\nimport {\n  ChevronLeftIcon,\n  ChevronRightIcon,\n  DotsHorizontalIcon,\n} from \"@/components/icons\";\n\nimport { cn } from \"@/lib/utils\";\nimport { buttonVariants, type ButtonProps } from \"@/components/ui/button\";\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<typeof Link>;\n\nconst PaginationLink = ({\n  className,\n  isActive,\n  size = \"icon\",\n  children,\n  ...props\n}: PaginationLinkProps) => (\n  <Link\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=\"h-4 w-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=\"h-4 w-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=\"h-4 w-4\" />\n    <span className=\"sr-only\">More pages</span>\n  </span>\n);\nPaginationEllipsis.displayName = \"PaginationEllipsis\";\n\nexport {\n  Pagination,\n  PaginationContent,\n  PaginationEllipsis,\n  PaginationItem,\n  PaginationLink,\n  PaginationNext,\n  PaginationPrevious,\n};\n"
  },
  {
    "path": "src/components/ui/skeleton.tsx",
    "content": "import { cn } from \"@/lib/utils\"\n\nfunction Skeleton({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) {\n  return (\n    <div\n      className={cn(\"animate-pulse rounded-md bg-muted\", className)}\n      {...props}\n    />\n  )\n}\n\nexport { Skeleton }\n"
  },
  {
    "path": "src/components/ui/sonner.tsx",
    "content": "\"use client\"\n\nimport { useTheme } from \"next-themes\"\nimport { Toaster as Sonner } from \"sonner\"\n\ntype ToasterProps = React.ComponentProps<typeof Sonner>\n\nconst Toaster = ({ ...props }: ToasterProps) => {\n  const { theme = \"system\" } = useTheme()\n\n  return (\n    <Sonner\n      theme={theme as ToasterProps[\"theme\"]}\n      className=\"toaster group\"\n      toastOptions={{\n        classNames: {\n          toast:\n            \"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg\",\n          description: \"group-[.toast]:text-muted-foreground\",\n          actionButton:\n            \"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground\",\n          cancelButton:\n            \"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground\",\n        },\n      }}\n      {...props}\n    />\n  )\n}\n\nexport { Toaster }\n"
  },
  {
    "path": "src/components/ui/tabs.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Tabs = TabsPrimitive.Root\n\nconst TabsList = React.forwardRef<\n  React.ElementRef<typeof TabsPrimitive.List>,\n  React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>\n>(({ className, ...props }, ref) => (\n  <TabsPrimitive.List\n    ref={ref}\n    className={cn(\n      \"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground\",\n      className\n    )}\n    {...props}\n  />\n))\nTabsList.displayName = TabsPrimitive.List.displayName\n\nconst TabsTrigger = React.forwardRef<\n  React.ElementRef<typeof TabsPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>\n>(({ className, ...props }, ref) => (\n  <TabsPrimitive.Trigger\n    ref={ref}\n    className={cn(\n      \"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow\",\n      className\n    )}\n    {...props}\n  />\n))\nTabsTrigger.displayName = TabsPrimitive.Trigger.displayName\n\nconst TabsContent = React.forwardRef<\n  React.ElementRef<typeof TabsPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>\n>(({ className, ...props }, ref) => (\n  <TabsPrimitive.Content\n    ref={ref}\n    className={cn(\n      \"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n      className\n    )}\n    {...props}\n  />\n))\nTabsContent.displayName = TabsPrimitive.Content.displayName\n\nexport { Tabs, TabsList, TabsTrigger, TabsContent }\n"
  },
  {
    "path": "src/components/ui/textarea.tsx",
    "content": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport interface TextareaProps\n  extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}\n\nconst Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(\n  ({ className, ...props }, ref) => {\n    return (\n      <textarea\n        className={cn(\n          \"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50\",\n          className\n        )}\n        ref={ref}\n        {...props}\n      />\n    )\n  }\n)\nTextarea.displayName = \"Textarea\"\n\nexport { Textarea }\n"
  },
  {
    "path": "src/config/subscriptions.ts",
    "content": "import { env } from \"@/env\";\n\nexport interface SubscriptionPlan {\n  name: string;\n  description: string;\n  features: string[];\n  stripePriceId: string;\n}\n\nexport const freePlan: SubscriptionPlan = {\n  name: \"Free\",\n  description: \"The free plan is limited to 3 posts.\",\n  features: [\"Up to 3 posts\", \"Limited support\"],\n  stripePriceId: \"\",\n};\n\nexport const proPlan: SubscriptionPlan = {\n  name: \"Pro\",\n  description: \"The Pro plan has unlimited posts.\",\n  features: [\"Unlimited posts\", \"Priority support\"],\n  stripePriceId: env.STRIPE_PRO_MONTHLY_PLAN_ID,\n};\n\nexport const subscriptionPlans = [freePlan, proPlan];\n"
  },
  {
    "path": "src/env.js",
    "content": "import { createEnv } from \"@t3-oss/env-nextjs\";\nimport { z } from \"zod\";\n\nexport const env = createEnv({\n  /**\n   * Specify your server-side environment variables schema here. This way you can ensure the app\n   * isn't built with invalid env vars.\n   */\n  server: {\n    DATABASE_URL: z\n      .string()\n      .url()\n      .refine(\n        (str) => !str.includes(\"YOUR_DATABASE_URL_HERE\"),\n        \"You forgot to change the default URL\",\n      ),\n    NODE_ENV: z.enum([\"development\", \"test\", \"production\"]).default(\"development\"),\n    MOCK_SEND_EMAIL: z.boolean().default(false),\n    DISCORD_CLIENT_ID: z.string().trim().min(1),\n    DISCORD_CLIENT_SECRET: z.string().trim().min(1),\n    SMTP_HOST: z.string().trim().min(1),\n    SMTP_PORT: z.number().int().min(1),\n    SMTP_USER: z.string().trim().min(1),\n    SMTP_PASSWORD: z.string().trim().min(1),\n    STRIPE_API_KEY: z.string().trim().min(1),\n    STRIPE_WEBHOOK_SECRET: z.string().trim().min(1),\n    STRIPE_PRO_MONTHLY_PLAN_ID: z.string().trim().min(1),\n  },\n\n  /**\n   * Specify your client-side environment variables schema here. This way you can ensure the app\n   * isn't built with invalid env vars. To expose them to the client, prefix them with\n   * `NEXT_PUBLIC_`.\n   */\n  client: {\n    // NEXT_PUBLIC_CLIENTVAR: z.string(),\n    NEXT_PUBLIC_APP_URL: z.string().url(),\n  },\n\n  /**\n   * You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g.\n   * middlewares) or client-side so we need to destruct manually.\n   */\n  runtimeEnv: {\n    // Server-side env vars\n    DATABASE_URL: process.env.DATABASE_URL,\n    NODE_ENV: process.env.NODE_ENV,\n    SMTP_HOST: process.env.SMTP_HOST,\n    SMTP_PORT: parseInt(process.env.SMTP_PORT ?? \"\"),\n    SMTP_USER: process.env.SMTP_USER,\n    SMTP_PASSWORD: process.env.SMTP_PASSWORD,\n    MOCK_SEND_EMAIL: process.env.MOCK_SEND_EMAIL === \"true\" || process.env.MOCK_SEND_EMAIL === \"1\",\n    DISCORD_CLIENT_ID: process.env.DISCORD_CLIENT_ID,\n    DISCORD_CLIENT_SECRET: process.env.DISCORD_CLIENT_SECRET,\n    STRIPE_API_KEY: process.env.STRIPE_API_KEY,\n    STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET,\n    STRIPE_PRO_MONTHLY_PLAN_ID: process.env.STRIPE_PRO_MONTHLY_PLAN_ID,\n    // Client-side env vars\n    NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,\n  },\n  /**\n   * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially\n   * useful for Docker builds.\n   */\n  skipValidation: !!process.env.SKIP_ENV_VALIDATION,\n  /**\n   * Makes it so that empty strings are treated as undefined.\n   * `SOME_VAR: z.string()` and `SOME_VAR=''` will throw an error.\n   */\n  emptyStringAsUndefined: true,\n});\n"
  },
  {
    "path": "src/lib/auth/actions.ts",
    "content": "\"use server\";\n\n/* eslint @typescript-eslint/no-explicit-any:0, @typescript-eslint/prefer-optional-chain:0 */\n\nimport { z } from \"zod\";\nimport { cookies } from \"next/headers\";\nimport { redirect } from \"next/navigation\";\nimport { generateId, Scrypt } from \"lucia\";\nimport { isWithinExpirationDate, TimeSpan, createDate } from \"oslo\";\nimport { generateRandomString, alphabet } from \"oslo/crypto\";\nimport { eq } from \"drizzle-orm\";\nimport { lucia } from \"@/lib/auth\";\nimport { db } from \"@/server/db\";\nimport {\n  loginSchema,\n  signupSchema,\n  type LoginInput,\n  type SignupInput,\n  resetPasswordSchema,\n} from \"@/lib/validators/auth\";\nimport { emailVerificationCodes, passwordResetTokens, users } from \"@/server/db/schema\";\nimport { sendMail, EmailTemplate } from \"@/lib/email\";\nimport { validateRequest } from \"@/lib/auth/validate-request\";\nimport { Paths } from \"../constants\";\nimport { env } from \"@/env\";\n\nexport interface ActionResponse<T> {\n  fieldError?: Partial<Record<keyof T, string | undefined>>;\n  formError?: string;\n}\n\nexport async function login(_: any, formData: FormData): Promise<ActionResponse<LoginInput>> {\n  const obj = Object.fromEntries(formData.entries());\n\n  const parsed = loginSchema.safeParse(obj);\n  if (!parsed.success) {\n    const err = parsed.error.flatten();\n    return {\n      fieldError: {\n        email: err.fieldErrors.email?.[0],\n        password: err.fieldErrors.password?.[0],\n      },\n    };\n  }\n\n  const { email, password } = parsed.data;\n\n  const existingUser = await db.query.users.findFirst({\n    where: (table, { eq }) => eq(table.email, email),\n  });\n\n  if (!existingUser || !existingUser?.hashedPassword) {\n    return {\n      formError: \"Incorrect email or password\",\n    };\n  }\n\n  const validPassword = await new Scrypt().verify(existingUser.hashedPassword, password);\n  if (!validPassword) {\n    return {\n      formError: \"Incorrect email or password\",\n    };\n  }\n\n  const session = await lucia.createSession(existingUser.id, {});\n  const sessionCookie = lucia.createSessionCookie(session.id);\n  cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);\n  return redirect(Paths.Dashboard);\n}\n\nexport async function signup(_: any, formData: FormData): Promise<ActionResponse<SignupInput>> {\n  const obj = Object.fromEntries(formData.entries());\n\n  const parsed = signupSchema.safeParse(obj);\n  if (!parsed.success) {\n    const err = parsed.error.flatten();\n    return {\n      fieldError: {\n        email: err.fieldErrors.email?.[0],\n        password: err.fieldErrors.password?.[0],\n      },\n    };\n  }\n\n  const { email, password } = parsed.data;\n\n  const existingUser = await db.query.users.findFirst({\n    where: (table, { eq }) => eq(table.email, email),\n    columns: { email: true },\n  });\n\n  if (existingUser) {\n    return {\n      formError: \"Cannot create account with that email\",\n    };\n  }\n\n  const userId = generateId(21);\n  const hashedPassword = await new Scrypt().hash(password);\n  await db.insert(users).values({\n    id: userId,\n    email,\n    hashedPassword,\n  });\n\n  const verificationCode = await generateEmailVerificationCode(userId, email);\n  await sendMail(email, EmailTemplate.EmailVerification, { code: verificationCode });\n\n  const session = await lucia.createSession(userId, {});\n  const sessionCookie = lucia.createSessionCookie(session.id);\n  cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);\n  return redirect(Paths.VerifyEmail);\n}\n\nexport async function logout(): Promise<{ error: string } | void> {\n  const { session } = await validateRequest();\n  if (!session) {\n    return {\n      error: \"No session found\",\n    };\n  }\n  await lucia.invalidateSession(session.id);\n  const sessionCookie = lucia.createBlankSessionCookie();\n  cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);\n  return redirect(\"/\");\n}\n\nexport async function resendVerificationEmail(): Promise<{\n  error?: string;\n  success?: boolean;\n}> {\n  const { user } = await validateRequest();\n  if (!user) {\n    return redirect(Paths.Login);\n  }\n  const lastSent = await db.query.emailVerificationCodes.findFirst({\n    where: (table, { eq }) => eq(table.userId, user.id),\n    columns: { expiresAt: true },\n  });\n\n  if (lastSent && isWithinExpirationDate(lastSent.expiresAt)) {\n    return {\n      error: `Please wait ${timeFromNow(lastSent.expiresAt)} before resending`,\n    };\n  }\n  const verificationCode = await generateEmailVerificationCode(user.id, user.email);\n  await sendMail(user.email, EmailTemplate.EmailVerification, { code: verificationCode });\n\n  return { success: true };\n}\n\nexport async function verifyEmail(_: any, formData: FormData): Promise<{ error: string } | void> {\n  const code = formData.get(\"code\");\n  if (typeof code !== \"string\" || code.length !== 8) {\n    return { error: \"Invalid code\" };\n  }\n  const { user } = await validateRequest();\n  if (!user) {\n    return redirect(Paths.Login);\n  }\n\n  const dbCode = await db.transaction(async (tx) => {\n    const item = await tx.query.emailVerificationCodes.findFirst({\n      where: (table, { eq }) => eq(table.userId, user.id),\n    });\n    if (item) {\n      await tx.delete(emailVerificationCodes).where(eq(emailVerificationCodes.id, item.id));\n    }\n    return item;\n  });\n\n  if (!dbCode || dbCode.code !== code) return { error: \"Invalid verification code\" };\n\n  if (!isWithinExpirationDate(dbCode.expiresAt)) return { error: \"Verification code expired\" };\n\n  if (dbCode.email !== user.email) return { error: \"Email does not match\" };\n\n  await lucia.invalidateUserSessions(user.id);\n  await db.update(users).set({ emailVerified: true }).where(eq(users.id, user.id));\n  const session = await lucia.createSession(user.id, {});\n  const sessionCookie = lucia.createSessionCookie(session.id);\n  cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);\n  redirect(Paths.Dashboard);\n}\n\nexport async function sendPasswordResetLink(\n  _: any,\n  formData: FormData,\n): Promise<{ error?: string; success?: boolean }> {\n  const email = formData.get(\"email\");\n  const parsed = z.string().trim().email().safeParse(email);\n  if (!parsed.success) {\n    return { error: \"Provided email is invalid.\" };\n  }\n  try {\n    const user = await db.query.users.findFirst({\n      where: (table, { eq }) => eq(table.email, parsed.data),\n    });\n\n    if (!user || !user.emailVerified) return { error: \"Provided email is invalid.\" };\n\n    const verificationToken = await generatePasswordResetToken(user.id);\n\n    const verificationLink = `${env.NEXT_PUBLIC_APP_URL}/reset-password/${verificationToken}`;\n\n    await sendMail(user.email, EmailTemplate.PasswordReset, { link: verificationLink });\n\n    return { success: true };\n  } catch (error) {\n    return { error: \"Failed to send verification email.\" };\n  }\n}\n\nexport async function resetPassword(\n  _: any,\n  formData: FormData,\n): Promise<{ error?: string; success?: boolean }> {\n  const obj = Object.fromEntries(formData.entries());\n\n  const parsed = resetPasswordSchema.safeParse(obj);\n\n  if (!parsed.success) {\n    const err = parsed.error.flatten();\n    return {\n      error: err.fieldErrors.password?.[0] ?? err.fieldErrors.token?.[0],\n    };\n  }\n  const { token, password } = parsed.data;\n\n  const dbToken = await db.transaction(async (tx) => {\n    const item = await tx.query.passwordResetTokens.findFirst({\n      where: (table, { eq }) => eq(table.id, token),\n    });\n    if (item) {\n      await tx.delete(passwordResetTokens).where(eq(passwordResetTokens.id, item.id));\n    }\n    return item;\n  });\n\n  if (!dbToken) return { error: \"Invalid password reset link\" };\n\n  if (!isWithinExpirationDate(dbToken.expiresAt)) return { error: \"Password reset link expired.\" };\n\n  await lucia.invalidateUserSessions(dbToken.userId);\n  const hashedPassword = await new Scrypt().hash(password);\n  await db.update(users).set({ hashedPassword }).where(eq(users.id, dbToken.userId));\n  const session = await lucia.createSession(dbToken.userId, {});\n  const sessionCookie = lucia.createSessionCookie(session.id);\n  cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);\n  redirect(Paths.Dashboard);\n}\n\nconst timeFromNow = (time: Date) => {\n  const now = new Date();\n  const diff = time.getTime() - now.getTime();\n  const minutes = Math.floor(diff / 1000 / 60);\n  const seconds = Math.floor(diff / 1000) % 60;\n  return `${minutes}m ${seconds}s`;\n};\n\nasync function generateEmailVerificationCode(userId: string, email: string): Promise<string> {\n  await db.delete(emailVerificationCodes).where(eq(emailVerificationCodes.userId, userId));\n  const code = generateRandomString(8, alphabet(\"0-9\")); // 8 digit code\n  await db.insert(emailVerificationCodes).values({\n    userId,\n    email,\n    code,\n    expiresAt: createDate(new TimeSpan(10, \"m\")), // 10 minutes\n  });\n  return code;\n}\n\nasync function generatePasswordResetToken(userId: string): Promise<string> {\n  await db.delete(passwordResetTokens).where(eq(passwordResetTokens.userId, userId));\n  const tokenId = generateId(40);\n  await db.insert(passwordResetTokens).values({\n    id: tokenId,\n    userId,\n    expiresAt: createDate(new TimeSpan(2, \"h\")),\n  });\n  return tokenId;\n}\n"
  },
  {
    "path": "src/lib/auth/index.ts",
    "content": "import { Lucia, TimeSpan } from \"lucia\";\nimport { Discord } from \"arctic\";\nimport { DrizzlePostgreSQLAdapter } from \"@lucia-auth/adapter-drizzle\";\nimport { env } from \"@/env.js\";\nimport { db } from \"@/server/db\";\nimport { sessions, users, type User as DbUser } from \"@/server/db/schema\";\nimport { absoluteUrl } from \"@/lib/utils\"\n\n// Uncomment the following lines if you are using nodejs 18 or lower. Not required in Node.js 20, CloudFlare Workers, Deno, Bun, and Vercel Edge Functions.\n// import { webcrypto } from \"node:crypto\";\n// globalThis.crypto = webcrypto as Crypto;\n\nconst adapter = new DrizzlePostgreSQLAdapter(db, sessions, users);\n\nexport const lucia = new Lucia(adapter, {\n  getSessionAttributes: (/* attributes */) => {\n    return {};\n  },\n  getUserAttributes: (attributes) => {\n    return {\n      id: attributes.id,\n      email: attributes.email,\n      emailVerified: attributes.emailVerified,\n      avatar: attributes.avatar,\n      createdAt: attributes.createdAt,\n      updatedAt: attributes.updatedAt,\n    };\n  },\n  sessionExpiresIn: new TimeSpan(30, \"d\"),\n  sessionCookie: {\n    name: \"session\",\n\n    expires: false, // session cookies have very long lifespan (2 years)\n    attributes: {\n      secure: env.NODE_ENV === \"production\",\n    },\n  },\n});\n\nexport const discord = new Discord(\n  env.DISCORD_CLIENT_ID,\n  env.DISCORD_CLIENT_SECRET,\n  absoluteUrl(\"/login/discord/callback\")\n);\n\ndeclare module \"lucia\" {\n  interface Register {\n    Lucia: typeof lucia;\n    DatabaseSessionAttributes: DatabaseSessionAttributes;\n    DatabaseUserAttributes: DatabaseUserAttributes;\n  }\n}\n\ninterface DatabaseSessionAttributes {}\ninterface DatabaseUserAttributes extends Omit<DbUser, \"hashedPassword\"> {}\n"
  },
  {
    "path": "src/lib/auth/validate-request.ts",
    "content": "import { cache } from \"react\";\nimport { cookies } from \"next/headers\";\nimport type { Session, User } from \"lucia\";\nimport { lucia } from \"@/lib/auth\";\n\n\nexport const uncachedValidateRequest = async (): Promise<\n  { user: User; session: Session } | { user: null; session: null }\n> => {\n  const sessionId = cookies().get(lucia.sessionCookieName)?.value ?? null;\n  if (!sessionId) {\n    return { user: null, session: null };\n  }\n  const result = await lucia.validateSession(sessionId);\n  // next.js throws when you attempt to set cookie when rendering page\n  try {\n    if (result.session && result.session.fresh) {\n      const sessionCookie = lucia.createSessionCookie(result.session.id);\n      cookies().set(\n        sessionCookie.name,\n        sessionCookie.value,\n        sessionCookie.attributes,\n      );\n    }\n    if (!result.session) {\n      const sessionCookie = lucia.createBlankSessionCookie();\n      cookies().set(\n        sessionCookie.name,\n        sessionCookie.value,\n        sessionCookie.attributes,\n      );\n    }\n  } catch {\n    console.error(\"Failed to set session cookie\");\n  }\n  return result;\n};\n\nexport const validateRequest = cache(uncachedValidateRequest);\n"
  },
  {
    "path": "src/lib/constants.ts",
    "content": "export const APP_TITLE = \"Acme\";\nexport const DATABASE_PREFIX = \"acme\";\nexport const TEST_DB_PREFIX = \"test_acme\";\nexport const EMAIL_SENDER = '\"Acme\" <noreply@acme.com>';\n\nexport enum Paths {\n  Home = \"/\",\n  Login = \"/login\",\n  Signup = \"/signup\",\n  Dashboard = \"/dashboard\",\n  VerifyEmail = \"/verify-email\",\n  ResetPassword = \"/reset-password\",\n}\n"
  },
  {
    "path": "src/lib/email/index.tsx",
    "content": "import \"server-only\";\n\nimport { EmailVerificationTemplate } from \"./templates/email-verification\";\nimport { ResetPasswordTemplate } from \"./templates/reset-password\";\nimport { render } from \"@react-email/render\";\nimport { env } from \"@/env\";\nimport { EMAIL_SENDER } from \"@/lib/constants\";\nimport { createTransport, type TransportOptions } from \"nodemailer\";\nimport type { ComponentProps } from \"react\";\nimport { logger } from \"../logger\";\n\nexport enum EmailTemplate {\n  EmailVerification = \"EmailVerification\",\n  PasswordReset = \"PasswordReset\",\n}\n\nexport type PropsMap = {\n  [EmailTemplate.EmailVerification]: ComponentProps<typeof EmailVerificationTemplate>;\n  [EmailTemplate.PasswordReset]: ComponentProps<typeof ResetPasswordTemplate>;\n};\n\nconst getEmailTemplate = <T extends EmailTemplate>(template: T, props: PropsMap[NoInfer<T>]) => {\n  switch (template) {\n    case EmailTemplate.EmailVerification:\n      return {\n        subject: \"Verify your email address\",\n        body: render(\n          <EmailVerificationTemplate {...(props as PropsMap[EmailTemplate.EmailVerification])} />,\n        ),\n      };\n    case EmailTemplate.PasswordReset:\n      return {\n        subject: \"Reset your password\",\n        body: render(\n          <ResetPasswordTemplate {...(props as PropsMap[EmailTemplate.PasswordReset])} />,\n        ),\n      };\n    default:\n      throw new Error(\"Invalid email template\");\n  }\n};\n\nconst smtpConfig = {\n  host: env.SMTP_HOST,\n  port: env.SMTP_PORT,\n  auth: {\n    user: env.SMTP_USER,\n    pass: env.SMTP_PASSWORD,\n  },\n};\n\nconst transporter = createTransport(smtpConfig as TransportOptions);\n\nexport const sendMail = async <T extends EmailTemplate>(\n  to: string,\n  template: T,\n  props: PropsMap[NoInfer<T>],\n) => {\n  if (env.MOCK_SEND_EMAIL) {\n    logger.info(\"📨 Email sent to:\", to, \"with template:\", template, \"and props:\", props);\n    return;\n  }\n\n  const { subject, body } = getEmailTemplate(template, props);\n\n  return transporter.sendMail({ from: EMAIL_SENDER, to, subject, html: body });\n};\n"
  },
  {
    "path": "src/lib/email/templates/email-verification.tsx",
    "content": "import { Body, Container, Head, Html, Preview, Section, Text } from \"@react-email/components\";\nimport { APP_TITLE } from \"@/lib/constants\";\n\nexport interface EmailVerificationTemplateProps {\n  code: string;\n}\n\nexport const EmailVerificationTemplate = ({ code }: EmailVerificationTemplateProps) => {\n  return (\n    <Html>\n      <Head />\n      <Preview>Verify your email address to complete your {APP_TITLE} registration</Preview>\n      <Body style={main}>\n        <Container style={container}>\n          <Section>\n            <Text style={title}>{APP_TITLE}</Text>\n            <Text style={text}>Hi,</Text>\n            <Text style={text}>\n              Thank you for registering for an account on {APP_TITLE}. To complete your\n              registration, please verify your your account by using the following code:\n            </Text>\n            <Text style={codePlaceholder}>{code}</Text>\n\n            <Text style={text}>Have a nice day!</Text>\n          </Section>\n        </Container>\n      </Body>\n    </Html>\n  );\n};\n\nconst main = {\n  backgroundColor: \"#f6f9fc\",\n  padding: \"10px 0\",\n};\n\nconst container = {\n  backgroundColor: \"#ffffff\",\n  border: \"1px solid #f0f0f0\",\n  padding: \"45px\",\n};\n\nconst text = {\n  fontSize: \"16px\",\n  fontFamily:\n    \"'Open Sans', 'HelveticaNeue-Light', 'Helvetica Neue Light', 'Helvetica Neue', Helvetica, Arial, 'Lucida Grande', sans-serif\",\n  fontWeight: \"300\",\n  color: \"#404040\",\n  lineHeight: \"26px\",\n};\n\nconst title = {\n  ...text,\n  fontSize: \"22px\",\n  fontWeight: \"700\",\n  lineHeight: \"32px\",\n};\n\nconst codePlaceholder = {\n  backgroundColor: \"#fbfbfb\",\n  border: \"1px solid #f0f0f0\",\n  borderRadius: \"4px\",\n  color: \"#1c1c1c\",\n  fontFamily: \"'Open Sans', 'Helvetica Neue', Arial\",\n  fontSize: \"15px\",\n  textDecoration: \"none\",\n  textAlign: \"center\" as const,\n  display: \"block\",\n  width: \"210px\",\n  padding: \"14px 7px\",\n};\n\n// const anchor = {\n//   textDecoration: \"underline\",\n// };\n"
  },
  {
    "path": "src/lib/email/templates/reset-password.tsx",
    "content": "import { render } from \"@react-email/render\";\nimport {\n  Body,\n  Button,\n  Container,\n  Head,\n  Html,\n  Preview,\n  Section,\n  Text,\n} from \"@react-email/components\";\nimport { APP_TITLE } from \"@/lib/constants\";\n\nexport interface ResetPasswordTemplateProps {\n  link: string;\n}\n\nexport const ResetPasswordTemplate = ({ link }: ResetPasswordTemplateProps) => {\n  return (\n    <Html>\n      <Head />\n      <Preview>Reset your password</Preview>\n      <Body style={main}>\n        <Container style={container}>\n          <Section>\n            <Text style={title}>{APP_TITLE}</Text>\n            <Text style={text}>Hi,</Text>\n            <Text style={text}>\n              Someone recently requested a password change for your {APP_TITLE} account. If this was\n              you, you can set a new password here:\n            </Text>\n            <Button style={button} href={link}>\n              Reset password\n            </Button>\n            <Text style={text}>\n              If you don&apos;t want to change your password or didn&apos;t request this, just\n              ignore and delete this message.\n            </Text>\n            <Text style={text}>\n              To keep your account secure, please don&apos;t forward this email to anyone.\n            </Text>\n            <Text style={text}>Have a nice day!</Text>\n          </Section>\n        </Container>\n      </Body>\n    </Html>\n  );\n};\n\nconst main = {\n  backgroundColor: \"#f6f9fc\",\n  padding: \"10px 0\",\n};\n\nconst container = {\n  backgroundColor: \"#ffffff\",\n  border: \"1px solid #f0f0f0\",\n  padding: \"45px\",\n};\n\nconst text = {\n  fontSize: \"16px\",\n  fontFamily:\n    \"'Open Sans', 'HelveticaNeue-Light', 'Helvetica Neue Light', 'Helvetica Neue', Helvetica, Arial, 'Lucida Grande', sans-serif\",\n  fontWeight: \"300\",\n  color: \"#404040\",\n  lineHeight: \"26px\",\n};\n\nconst title = {\n  ...text,\n  fontSize: \"22px\",\n  fontWeight: \"700\",\n  lineHeight: \"32px\",\n};\n\nconst button = {\n  backgroundColor: \"#09090b\",\n  borderRadius: \"4px\",\n  color: \"#fafafa\",\n  fontFamily: \"'Open Sans', 'Helvetica Neue', Arial\",\n  fontSize: \"15px\",\n  textDecoration: \"none\",\n  textAlign: \"center\" as const,\n  display: \"block\",\n  width: \"210px\",\n  padding: \"14px 7px\",\n};\n\n// const anchor = {\n//   textDecoration: \"underline\",\n// };\n"
  },
  {
    "path": "src/lib/fonts.ts",
    "content": "import \"@/styles/globals.css\";\n\nimport { Inter as FontSans } from \"next/font/google\";\n\nexport const fontSans = FontSans({\n  subsets: [\"latin\"],\n  variable: \"--font-sans\",\n});\n"
  },
  {
    "path": "src/lib/hooks/use-debounce.ts",
    "content": "import { useEffect, useState } from \"react\";\n\nexport function useDebounce<T>(value: T, delay: number) {\n  const [debouncedValue, setDebouncedValue] = useState(value);\n\n  useEffect(() => {\n    const handler = setTimeout(() => {\n      setDebouncedValue(value);\n    }, delay);\n\n    return () => {\n      clearTimeout(handler);\n    };\n  }, [value, delay]);\n\n  return debouncedValue;\n}\n"
  },
  {
    "path": "src/lib/hooks/use-media-query.ts",
    "content": "import * as React from \"react\";\n\nexport function useMediaQuery(query: string) {\n  const [value, setValue] = React.useState(false);\n\n  React.useEffect(() => {\n    function onChange(event: MediaQueryListEvent) {\n      setValue(event.matches);\n    }\n\n    const result = matchMedia(query);\n    result.addEventListener(\"change\", onChange);\n    setValue(result.matches);\n\n    return () => result.removeEventListener(\"change\", onChange);\n  }, [query]);\n\n  return value;\n}\n"
  },
  {
    "path": "src/lib/logger.ts",
    "content": "import { env } from \"@/env\";\nimport * as fs from \"fs\";\nimport * as path from \"path\";\n\nenum LogLevel {\n  DEBUG = \"DEBUG\",\n  INFO = \"INFO\",\n  WARN = \"WARN\",\n  ERROR = \"ERROR\",\n}\n\nclass Logger {\n  private level: LogLevel;\n  private logFilePath: string;\n\n  constructor(level: LogLevel = LogLevel.INFO, logFilePath = \"application.log\") {\n    this.level = level;\n    this.logFilePath = path.resolve(logFilePath);\n  }\n\n  private getTimestamp(): string {\n    return new Date().toISOString();\n  }\n\n  private formatMessage(level: LogLevel, args: unknown[]): string {\n    const message = args\n      .map((arg) => (typeof arg === \"object\" ? JSON.stringify(arg) : arg))\n      .join(\" \");\n\n    if (env.NODE_ENV === \"development\") {\n      console.log(message);\n    }\n\n    return `[${this.getTimestamp()}] [${level}] ${message}`;\n  }\n\n  private log(level: LogLevel, ...args: unknown[]): void {\n    if (this.shouldLog(level)) {\n      const logMessage = this.formatMessage(level, args) + \"\\n\";\n      fs.appendFile(this.logFilePath, logMessage, (err) => {\n        if (err) throw err;\n      });\n    }\n  }\n\n  private shouldLog(level: LogLevel): boolean {\n    const levels = [LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARN, LogLevel.ERROR];\n    return levels.indexOf(level) >= levels.indexOf(this.level);\n  }\n\n  debug(...args: unknown[]): void {\n    this.log(LogLevel.DEBUG, ...args);\n  }\n\n  info(...args: unknown[]): void {\n    this.log(LogLevel.INFO, ...args);\n  }\n\n  warn(...args: unknown[]): void {\n    this.log(LogLevel.WARN, ...args);\n  }\n\n  error(...args: unknown[]): void {\n    this.log(LogLevel.ERROR, ...args);\n  }\n}\n\nexport const logger = new Logger(env.NODE_ENV === \"development\" ? LogLevel.DEBUG : LogLevel.INFO);\n"
  },
  {
    "path": "src/lib/stripe.ts",
    "content": "import { env } from \"@/env\";\nimport Stripe from \"stripe\";\n\nexport const stripe = new Stripe(env.STRIPE_API_KEY, {\n  apiVersion: \"2023-10-16\",\n  typescript: true,\n});\n"
  },
  {
    "path": "src/lib/utils.ts",
    "content": "import { env } from \"@/env\";\nimport { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n\nexport const getExceptionType = (error: unknown) => {\n  const UnknownException = {\n    type: \"UnknownException\",\n    status: 500,\n    message: \"An unknown error occurred\",\n  };\n\n  if (!error) return UnknownException;\n\n  if ((error as Record<string, unknown>).name === \"DatabaseError\") {\n    return {\n      type: \"DatabaseException\",\n      status: 400,\n      message: \"Duplicate key entry\",\n    };\n  }\n\n  return UnknownException;\n};\n\nexport function formatDate(\n  date: Date | string | number,\n  options: Intl.DateTimeFormatOptions = {\n    month: \"long\",\n    day: \"numeric\",\n    year: \"numeric\",\n  },\n) {\n  return new Intl.DateTimeFormat(\"en-US\", {\n    ...options,\n  }).format(new Date(date));\n}\n\nexport function formatPrice(\n  price: number | string,\n  options: Intl.NumberFormatOptions = {},\n) {\n  return new Intl.NumberFormat(\"en-US\", {\n    style: \"currency\",\n    currency: options.currency ?? \"USD\",\n    notation: options.notation ?? \"compact\",\n    ...options,\n  }).format(Number(price));\n}\n\nexport function absoluteUrl(path: string) {\n  return new URL(path, env.NEXT_PUBLIC_APP_URL).href\n}\n"
  },
  {
    "path": "src/lib/validators/auth.ts",
    "content": "import { z } from \"zod\";\n\nexport const signupSchema = z.object({\n  email: z.string().email(\"Please enter a valid email\"),\n  password: z.string().min(1, \"Please provide your password.\").max(255),\n});\nexport type SignupInput = z.infer<typeof signupSchema>;\n\nexport const loginSchema = z.object({\n  email: z.string().email(\"Please enter a valid email.\"),\n  password: z\n    .string()\n    .min(8, \"Password is too short. Minimum 8 characters required.\")\n    .max(255),\n});\nexport type LoginInput = z.infer<typeof loginSchema>;\n\nexport const forgotPasswordSchema = z.object({\n  email: z.string().email(),\n});\nexport type ForgotPasswordInput = z.infer<typeof forgotPasswordSchema>;\n\nexport const resetPasswordSchema = z.object({\n  token: z.string().min(1, \"Invalid token\"),\n  password: z.string().min(8, \"Password is too short\").max(255),\n});\nexport type ResetPasswordInput = z.infer<typeof resetPasswordSchema>;\n"
  },
  {
    "path": "src/middleware.ts",
    "content": "// middleware.ts\nimport { verifyRequestOrigin } from \"lucia\";\nimport { NextResponse } from \"next/server\";\nimport type { NextRequest } from \"next/server\";\n\nexport async function middleware(request: NextRequest): Promise<NextResponse> {\n  if (request.method === \"GET\") {\n    return NextResponse.next();\n  }\n  const originHeader = request.headers.get(\"Origin\");\n  const hostHeader = request.headers.get(\"Host\");\n  if (\n    !originHeader ||\n    !hostHeader ||\n    !verifyRequestOrigin(originHeader, [hostHeader])\n  ) {\n    return new NextResponse(null, {\n      status: 403,\n    });\n  }\n  return NextResponse.next();\n}\n\nexport const config = {\n  matcher: [\n    \"/((?!api|static|.*\\\\..*|_next|favicon.ico|sitemap.xml|robots.txt).*)\",\n  ],\n};\n"
  },
  {
    "path": "src/server/api/root.ts",
    "content": "import { postRouter } from \"./routers/post/post.procedure\";\nimport { stripeRouter } from \"./routers/stripe/stripe.procedure\";\nimport { userRouter } from \"./routers/user/user.procedure\";\nimport { createTRPCRouter } from \"./trpc\";\n\nexport const appRouter = createTRPCRouter({\n  user: userRouter,\n  post: postRouter,\n  stripe: stripeRouter,\n});\n\nexport type AppRouter = typeof appRouter;\n"
  },
  {
    "path": "src/server/api/routers/post/post.input.ts",
    "content": "import { z } from \"zod\";\n\nexport const listPostsSchema = z.object({\n  page: z.number().int().default(1),\n  perPage: z.number().int().default(12),\n});\nexport type ListPostsInput = z.infer<typeof listPostsSchema>;\n\nexport const getPostSchema = z.object({\n  id: z.string(),\n});\nexport type GetPostInput = z.infer<typeof getPostSchema>;\n\nexport const createPostSchema = z.object({\n  title: z.string().min(3).max(255),\n  excerpt: z.string().min(3).max(255),\n  content: z.string().min(3),\n});\nexport type CreatePostInput = z.infer<typeof createPostSchema>;\n\nexport const updatePostSchema = createPostSchema.extend({\n  id: z.string(),\n});\nexport type UpdatePostInput = z.infer<typeof updatePostSchema>;\n\nexport const deletePostSchema = z.object({\n  id: z.string(),\n});\nexport type DeletePostInput = z.infer<typeof deletePostSchema>;\n\nexport const myPostsSchema = z.object({\n  page: z.number().int().default(1),\n  perPage: z.number().int().default(12),\n});\nexport type MyPostsInput = z.infer<typeof myPostsSchema>;\n"
  },
  {
    "path": "src/server/api/routers/post/post.procedure.ts",
    "content": "import { createTRPCRouter, protectedProcedure } from \"../../trpc\";\nimport * as inputs from \"./post.input\";\nimport * as services from \"./post.service\";\n\nexport const postRouter = createTRPCRouter({\n  list: protectedProcedure\n    .input(inputs.listPostsSchema)\n    .query(({ ctx, input }) => services.listPosts(ctx, input)),\n\n  get: protectedProcedure\n    .input(inputs.getPostSchema)\n    .query(({ ctx, input }) => services.getPost(ctx, input)),\n\n  create: protectedProcedure\n    .input(inputs.createPostSchema)\n    .mutation(({ ctx, input }) => services.createPost(ctx, input)),\n\n  update: protectedProcedure\n    .input(inputs.updatePostSchema)\n    .mutation(({ ctx, input }) => services.updatePost(ctx, input)),\n\n  delete: protectedProcedure\n    .input(inputs.deletePostSchema)\n    .mutation(async ({ ctx, input }) => services.deletePost(ctx, input)),\n\n  myPosts: protectedProcedure\n    .input(inputs.myPostsSchema)\n    .query(({ ctx, input }) => services.myPosts(ctx, input)),\n});\n"
  },
  {
    "path": "src/server/api/routers/post/post.service.ts",
    "content": "import { generateId } from \"lucia\";\nimport type { ProtectedTRPCContext } from \"../../trpc\";\nimport type {\n  CreatePostInput,\n  DeletePostInput,\n  GetPostInput,\n  ListPostsInput,\n  MyPostsInput,\n  UpdatePostInput,\n} from \"./post.input\";\nimport { posts } from \"@/server/db/schema\";\nimport { eq } from \"drizzle-orm\";\n\nexport const listPosts = async (ctx: ProtectedTRPCContext, input: ListPostsInput) => {\n  return ctx.db.query.posts.findMany({\n    where: (table, { eq }) => eq(table.status, \"published\"),\n    offset: (input.page - 1) * input.perPage,\n    limit: input.perPage,\n    orderBy: (table, { desc }) => desc(table.createdAt),\n    columns: {\n      id: true,\n      title: true,\n      excerpt: true,\n      status: true,\n      createdAt: true,\n    },\n    with: { user: { columns: { email: true } } },\n  });\n};\n\nexport const getPost = async (ctx: ProtectedTRPCContext, { id }: GetPostInput) => {\n  return ctx.db.query.posts.findFirst({\n    where: (table, { eq }) => eq(table.id, id),\n    with: { user: { columns: { email: true } } },\n  });\n};\n\nexport const createPost = async (ctx: ProtectedTRPCContext, input: CreatePostInput) => {\n  const id = generateId(15);\n\n  await ctx.db.insert(posts).values({\n    id,\n    userId: ctx.user.id,\n    title: input.title,\n    excerpt: input.excerpt,\n    content: input.content,\n  });\n\n  return { id };\n};\n\nexport const updatePost = async (ctx: ProtectedTRPCContext, input: UpdatePostInput) => {\n  const [item] = await ctx.db\n    .update(posts)\n    .set({\n      title: input.title,\n      excerpt: input.excerpt,\n      content: input.content,\n    })\n    .where(eq(posts.id, input.id))\n    .returning();\n\n  return item;\n};\n\nexport const deletePost = async (ctx: ProtectedTRPCContext, { id }: DeletePostInput) => {\n  const [item] = await ctx.db.delete(posts).where(eq(posts.id, id)).returning();\n  return item;\n};\n\nexport const myPosts = async (ctx: ProtectedTRPCContext, input: MyPostsInput) => {\n  return ctx.db.query.posts.findMany({\n    where: (table, { eq }) => eq(table.userId, ctx.user.id),\n    offset: (input.page - 1) * input.perPage,\n    limit: input.perPage,\n    orderBy: (table, { desc }) => desc(table.createdAt),\n    columns: {\n      id: true,\n      title: true,\n      excerpt: true,\n      status: true,\n      createdAt: true,\n    },\n  });\n};\n"
  },
  {
    "path": "src/server/api/routers/stripe/stripe.input.ts",
    "content": "import { z } from \"zod\";\n\nexport const manageSubscriptionSchema = z.object({\n  stripePriceId: z.string(),\n  stripeCustomerId: z.string().optional().nullable(),\n  stripeSubscriptionId: z.string().optional().nullable(),\n  isPro: z.boolean(),\n});\n\nexport type ManageSubscriptionInput = z.infer<typeof manageSubscriptionSchema>;\n"
  },
  {
    "path": "src/server/api/routers/stripe/stripe.procedure.ts",
    "content": "import { createTRPCRouter, protectedProcedure } from \"../../trpc\";\nimport * as services from \"./stripe.service\";\nimport * as inputs from \"./stripe.input\";\n\nexport const stripeRouter = createTRPCRouter({\n  getPlans: protectedProcedure.query(({ ctx }) => services.getStripePlans(ctx)),\n\n  getPlan: protectedProcedure.query(({ ctx }) => services.getStripePlan(ctx)),\n\n  managePlan: protectedProcedure\n    .input(inputs.manageSubscriptionSchema)\n    .mutation(({ ctx, input }) => services.manageSubscription(ctx, input)),\n});\n"
  },
  {
    "path": "src/server/api/routers/stripe/stripe.service.ts",
    "content": "import { freePlan, proPlan, subscriptionPlans } from \"@/config/subscriptions\";\nimport type { ProtectedTRPCContext } from \"../../trpc\";\nimport { stripe } from \"@/lib/stripe\";\nimport { absoluteUrl, formatPrice } from \"@/lib/utils\";\nimport type { ManageSubscriptionInput } from \"./stripe.input\";\n\nexport const getStripePlans = async (ctx: ProtectedTRPCContext) => {\n  try {\n    const user = await ctx.db.query.users.findFirst({\n      where: (table, { eq }) => eq(table.id, ctx.user.id),\n      columns: {\n        id: true,\n      },\n    });\n\n    if (!user) {\n      throw new Error(\"User not found.\");\n    }\n\n    const proPrice = await stripe.prices.retrieve(proPlan.stripePriceId);\n\n    return subscriptionPlans.map((plan) => {\n      return {\n        ...plan,\n        price:\n          plan.stripePriceId === proPlan.stripePriceId\n            ? formatPrice((proPrice.unit_amount ?? 0) / 100, {\n                currency: proPrice.currency,\n              })\n            : formatPrice(0 / 100, { currency: proPrice.currency }),\n      };\n    });\n  } catch (err) {\n    console.error(err);\n    return [];\n  }\n};\n\nexport const getStripePlan = async (ctx: ProtectedTRPCContext) => {\n  try {\n    const user = await ctx.db.query.users.findFirst({\n      where: (table, { eq }) => eq(table.id, ctx.user.id),\n      columns: {\n        stripePriceId: true,\n        stripeCurrentPeriodEnd: true,\n        stripeSubscriptionId: true,\n        stripeCustomerId: true,\n      },\n    });\n\n    if (!user) {\n      throw new Error(\"User not found.\");\n    }\n\n    // Check if user is on a pro plan\n    const isPro =\n      !!user.stripePriceId &&\n      (user.stripeCurrentPeriodEnd?.getTime() ?? 0) + 86_400_000 > Date.now();\n\n    const plan = isPro ? proPlan : freePlan;\n\n    // Check if user has canceled subscription\n    let isCanceled = false;\n    if (isPro && !!user.stripeSubscriptionId) {\n      const stripePlan = await stripe.subscriptions.retrieve(user.stripeSubscriptionId);\n      isCanceled = stripePlan.cancel_at_period_end;\n    }\n\n    return {\n      ...plan,\n      stripeSubscriptionId: user.stripeSubscriptionId,\n      stripeCurrentPeriodEnd: user.stripeCurrentPeriodEnd,\n      stripeCustomerId: user.stripeCustomerId,\n      isPro,\n      isCanceled,\n    };\n  } catch (err) {\n    console.error(err);\n    return null;\n  }\n};\n\nexport const manageSubscription = async (\n  ctx: ProtectedTRPCContext,\n  input: ManageSubscriptionInput,\n) => {\n  const billingUrl = absoluteUrl(\"/dashboard/billing\");\n\n  const user = await ctx.db.query.users.findFirst({\n    where: (table, { eq }) => eq(table.id, ctx.user.id),\n    columns: {\n      id: true,\n      email: true,\n      stripeCustomerId: true,\n      stripeSubscriptionId: true,\n      stripePriceId: true,\n    },\n  });\n\n  if (!user) {\n    throw new Error(\"User not found.\");\n  }\n\n  // If the user is already subscribed to a plan, we redirect them to the Stripe billing portal\n  if (input.isPro && input.stripeCustomerId) {\n    const stripeSession = await ctx.stripe.billingPortal.sessions.create({\n      customer: input.stripeCustomerId,\n      return_url: billingUrl,\n    });\n\n    return {\n      url: stripeSession.url,\n    };\n  }\n\n  // If the user is not subscribed to a plan, we create a Stripe Checkout session\n  const stripeSession = await ctx.stripe.checkout.sessions.create({\n    success_url: billingUrl,\n    cancel_url: billingUrl,\n    payment_method_types: [\"card\"],\n    mode: \"subscription\",\n    billing_address_collection: \"auto\",\n    customer_email: user.email,\n    line_items: [\n      {\n        price: input.stripePriceId,\n        quantity: 1,\n      },\n    ],\n    metadata: {\n      userId: user.id,\n    },\n  });\n\n  return {\n    url: stripeSession.url,\n  };\n};\n"
  },
  {
    "path": "src/server/api/routers/user/user.procedure.ts",
    "content": "import { protectedProcedure, createTRPCRouter } from \"../../trpc\";\n\nexport const userRouter = createTRPCRouter({\n  get: protectedProcedure.query(({ ctx }) => ctx.user),\n});\n"
  },
  {
    "path": "src/server/api/trpc.ts",
    "content": "/**\n * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS:\n * 1. You want to modify request context (see Part 1).\n * 2. You want to create a new middleware or type of procedure (see Part 3).\n *\n * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will\n * need to use are documented accordingly near the end.\n */\n\nimport { uncachedValidateRequest } from \"@/lib/auth/validate-request\";\nimport { stripe } from \"@/lib/stripe\";\nimport { db } from \"@/server/db\";\nimport { initTRPC, TRPCError, type inferAsyncReturnType } from \"@trpc/server\";\nimport superjson from \"superjson\";\nimport { ZodError } from \"zod\";\n\n/**\n * 1. CONTEXT\n *\n * This section defines the \"contexts\" that are available in the backend API.\n *\n * These allow you to access things when processing a request, like the database, the session, etc.\n *\n * This helper generates the \"internals\" for a tRPC context. The API handler and RSC clients each\n * wrap this and provides the required context.\n *\n * @see https://trpc.io/docs/server/context\n */\nexport const createTRPCContext = async (opts: { headers: Headers }) => {\n  const { session, user } = await uncachedValidateRequest();\n  return {\n    session,\n    user,\n    db,\n    headers: opts.headers,\n    stripe: stripe,\n  };\n};\n\n/**\n * 2. INITIALIZATION\n *\n * This is where the tRPC API is initialized, connecting the context and transformer. We also parse\n * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation\n * errors on the backend.\n */\nconst t = initTRPC.context<typeof createTRPCContext>().create({\n  transformer: superjson,\n  errorFormatter({ shape, error }) {\n    return {\n      ...shape,\n      data: {\n        ...shape.data,\n        zodError: error.cause instanceof ZodError ? error.cause.flatten() : null,\n      },\n    };\n  },\n});\n\n/**\n * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT)\n *\n * These are the pieces you use to build your tRPC API. You should import these a lot in the\n * \"/src/server/api/routers\" directory.\n */\n\n/**\n * This is how you create new routers and sub-routers in your tRPC API.\n *\n * @see https://trpc.io/docs/router\n */\nexport const createTRPCRouter = t.router;\n\n/**\n * Public (unauthenticated) procedure\n *\n * This is the base piece you use to build new queries and mutations on your tRPC API. It does not\n * guarantee that a user querying is authorized, but you can still access user session data if they\n * are logged in.\n */\nexport const publicProcedure = t.procedure;\n\n/**\n * Protected (authenticated) procedure\n *\n * If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies\n * the session is valid and guarantees `ctx.session.user` is not null.\n *\n * @see https://trpc.io/docs/procedures\n */\nexport const protectedProcedure = t.procedure.use(({ ctx, next }) => {\n  if (!ctx.session || !ctx.user) {\n    throw new TRPCError({ code: \"UNAUTHORIZED\" });\n  }\n  return next({\n    ctx: {\n      // infers the `session` and `user` as non-nullable\n      session: { ...ctx.session },\n      user: { ...ctx.user },\n    },\n  });\n});\n\nexport type TRPCContext = inferAsyncReturnType<typeof createTRPCContext>;\nexport type ProtectedTRPCContext = TRPCContext & {\n  user: NonNullable<TRPCContext[\"user\"]>;\n  session: NonNullable<TRPCContext[\"session\"]>;\n};\n"
  },
  {
    "path": "src/server/db/index.ts",
    "content": "import { drizzle } from \"drizzle-orm/postgres-js\";\nimport postgres from \"postgres\";\nimport { env } from \"@/env\";\nimport * as schema from \"./schema\";\n\nexport const connection = postgres(env.DATABASE_URL, {\n  max_lifetime: 10, // Remove this line if you're deploying to Docker / VPS\n  // idle_timeout: 20, // Uncomment this line if you're deploying to Docker / VPS\n});\n\nexport const db = drizzle(connection, { schema });\n"
  },
  {
    "path": "src/server/db/schema.ts",
    "content": "import { relations } from \"drizzle-orm\";\nimport {\n  pgTableCreator,\n  serial,\n  boolean,\n  index,\n  text,\n  timestamp,\n  varchar,\n} from \"drizzle-orm/pg-core\";\nimport { DATABASE_PREFIX as prefix } from \"@/lib/constants\";\n\nexport const pgTable = pgTableCreator((name) => `${prefix}_${name}`);\n\nexport const users = pgTable(\n  \"users\",\n  {\n    id: varchar(\"id\", { length: 21 }).primaryKey(),\n    discordId: varchar(\"discord_id\", { length: 255 }).unique(),\n    email: varchar(\"email\", { length: 255 }).unique().notNull(),\n    emailVerified: boolean(\"email_verified\").default(false).notNull(),\n    hashedPassword: varchar(\"hashed_password\", { length: 255 }),\n    avatar: varchar(\"avatar\", { length: 255 }),\n    stripeSubscriptionId: varchar(\"stripe_subscription_id\", { length: 191 }),\n    stripePriceId: varchar(\"stripe_price_id\", { length: 191 }),\n    stripeCustomerId: varchar(\"stripe_customer_id\", { length: 191 }),\n    stripeCurrentPeriodEnd: timestamp(\"stripe_current_period_end\"),\n    createdAt: timestamp(\"created_at\").defaultNow().notNull(),\n    updatedAt: timestamp(\"updated_at\", { mode: \"date\" }).$onUpdate(() => new Date()),\n  },\n  (t) => ({\n    emailIdx: index(\"user_email_idx\").on(t.email),\n    discordIdx: index(\"user_discord_idx\").on(t.discordId),\n  }),\n);\n\nexport type User = typeof users.$inferSelect;\nexport type NewUser = typeof users.$inferInsert;\n\nexport const sessions = pgTable(\n  \"sessions\",\n  {\n    id: varchar(\"id\", { length: 255 }).primaryKey(),\n    userId: varchar(\"user_id\", { length: 21 }).notNull(),\n    expiresAt: timestamp(\"expires_at\", { withTimezone: true, mode: \"date\" }).notNull(),\n  },\n  (t) => ({\n    userIdx: index(\"session_user_idx\").on(t.userId),\n  }),\n);\n\nexport const emailVerificationCodes = pgTable(\n  \"email_verification_codes\",\n  {\n    id: serial(\"id\").primaryKey(),\n    userId: varchar(\"user_id\", { length: 21 }).unique().notNull(),\n    email: varchar(\"email\", { length: 255 }).notNull(),\n    code: varchar(\"code\", { length: 8 }).notNull(),\n    expiresAt: timestamp(\"expires_at\", { withTimezone: true, mode: \"date\" }).notNull(),\n  },\n  (t) => ({\n    userIdx: index(\"verification_code_user_idx\").on(t.userId),\n    emailIdx: index(\"verification_code_email_idx\").on(t.email),\n  }),\n);\n\nexport const passwordResetTokens = pgTable(\n  \"password_reset_tokens\",\n  {\n    id: varchar(\"id\", { length: 40 }).primaryKey(),\n    userId: varchar(\"user_id\", { length: 21 }).notNull(),\n    expiresAt: timestamp(\"expires_at\", { withTimezone: true, mode: \"date\" }).notNull(),\n  },\n  (t) => ({\n    userIdx: index(\"password_token_user_idx\").on(t.userId),\n  }),\n);\n\nexport const posts = pgTable(\n  \"posts\",\n  {\n    id: varchar(\"id\", { length: 15 }).primaryKey(),\n    userId: varchar(\"user_id\", { length: 255 }).notNull(),\n    title: varchar(\"title\", { length: 255 }).notNull(),\n    excerpt: varchar(\"excerpt\", { length: 255 }).notNull(),\n    content: text(\"content\").notNull(),\n    status: varchar(\"status\", { length: 10, enum: [\"draft\", \"published\"] })\n      .default(\"draft\")\n      .notNull(),\n    tags: varchar(\"tags\", { length: 255 }),\n    createdAt: timestamp(\"created_at\").defaultNow().notNull(),\n    updatedAt: timestamp(\"updated_at\", { mode: \"date\" }).$onUpdate(() => new Date()),\n  },\n  (t) => ({\n    userIdx: index(\"post_user_idx\").on(t.userId),\n    createdAtIdx: index(\"post_created_at_idx\").on(t.createdAt),\n  }),\n);\n\nexport const postRelations = relations(posts, ({ one }) => ({\n  user: one(users, {\n    fields: [posts.userId],\n    references: [users.id],\n  }),\n}));\n\nexport type Post = typeof posts.$inferSelect;\nexport type NewPost = typeof posts.$inferInsert;\n"
  },
  {
    "path": "src/styles/globals.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer base {\n  :root {\n    --background: 0 0% 98%;\n    --foreground: 240 10% 3.9%;\n\n    --card: 0 0% 100%;\n    --card-foreground: 240 10% 3.9%;\n\n    --popover: 0 0% 100%;\n    --popover-foreground: 240 10% 3.9%;\n\n    --primary: 240 5.9% 10%;\n    --primary-foreground: 0 0% 98%;\n\n    --secondary: 240 4.8% 95.9%;\n    --secondary-foreground: 240 5.9% 10%;\n\n    --muted: 240 4.8% 95.9%;\n    --muted-foreground: 240 3.8% 46.1%;\n\n    --accent: 240 4.8% 95.9%;\n    --accent-foreground: 240 5.9% 10%;\n\n    --destructive: 0 84.2% 60.2%;\n    --destructive-foreground: 0 0% 98%;\n\n    --border: 240 5.9% 90%;\n    --input: 240 5.9% 90%;\n    --ring: 240 10% 3.9%;\n\n    --radius: 0.5rem;\n  }\n\n  .dark {\n    --background: 240 10% 3.9%;\n    --foreground: 0 0% 98%;\n\n    --card: 240 10% 3.9%;\n    --card-foreground: 0 0% 98%;\n\n    --popover: 240 10% 3.9%;\n    --popover-foreground: 0 0% 98%;\n\n    --primary: 0 0% 98%;\n    --primary-foreground: 240 5.9% 10%;\n\n    --secondary: 240 3.7% 15.9%;\n    --secondary-foreground: 0 0% 98%;\n\n    --muted: 240 3.7% 15.9%;\n    --muted-foreground: 240 5% 64.9%;\n\n    --accent: 240 3.7% 15.9%;\n    --accent-foreground: 0 0% 98%;\n\n    --destructive: 0 62.8% 40.6%;\n    --destructive-foreground: 0 0% 98%;\n\n    --border: 240 3.7% 15.9%;\n    --input: 240 3.7% 15.9%;\n    --ring: 240 4.9% 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\n@layer utilities {\n  .text-balance {\n    text-wrap: balance;\n  }\n}\n\n.animated-spinner {\n  transform-origin: center;\n  animation: loader-spin 0.75s step-end infinite;\n}\n@keyframes loader-spin {\n  8.3% {\n    transform: rotate(30deg);\n  }\n  16.6% {\n    transform: rotate(60deg);\n  }\n  25% {\n    transform: rotate(90deg);\n  }\n  33.3% {\n    transform: rotate(120deg);\n  }\n  41.6% {\n    transform: rotate(150deg);\n  }\n  50% {\n    transform: rotate(180deg);\n  }\n  58.3% {\n    transform: rotate(210deg);\n  }\n  66.6% {\n    transform: rotate(240deg);\n  }\n  75% {\n    transform: rotate(270deg);\n  }\n  83.3% {\n    transform: rotate(300deg);\n  }\n  91.6% {\n    transform: rotate(330deg);\n  }\n  100% {\n    transform: rotate(360deg);\n  }\n}\n"
  },
  {
    "path": "src/trpc/react.tsx",
    "content": "\"use client\";\n\nimport { QueryClient, QueryClientProvider } from \"@tanstack/react-query\";\nimport { loggerLink, unstable_httpBatchStreamLink } from \"@trpc/client\";\nimport { createTRPCReact } from \"@trpc/react-query\";\nimport { useState } from \"react\";\n\nimport { type AppRouter } from \"@/server/api/root\";\nimport { getUrl, transformer } from \"./shared\";\n\nexport const api = createTRPCReact<AppRouter>();\n\nexport function TRPCReactProvider(props: { children: React.ReactNode }) {\n  const [queryClient] = useState(() => new QueryClient());\n\n  const [trpcClient] = useState(() =>\n    api.createClient({\n      transformer,\n      links: [\n        loggerLink({\n          enabled: (op) =>\n            process.env.NODE_ENV === \"development\" ||\n            (op.direction === \"down\" && op.result instanceof Error),\n        }),\n        unstable_httpBatchStreamLink({\n          url: getUrl(),\n        }),\n      ],\n    }),\n  );\n\n  return (\n    <QueryClientProvider client={queryClient}>\n      <api.Provider client={trpcClient} queryClient={queryClient}>\n        {props.children}\n      </api.Provider>\n    </QueryClientProvider>\n  );\n}\n"
  },
  {
    "path": "src/trpc/server.ts",
    "content": "import \"server-only\";\n\nimport {\n  createTRPCProxyClient,\n  loggerLink,\n  TRPCClientError,\n} from \"@trpc/client\";\nimport { callProcedure } from \"@trpc/server\";\nimport { observable } from \"@trpc/server/observable\";\nimport { type TRPCErrorResponse } from \"@trpc/server/rpc\";\nimport { cache } from \"react\";\nimport { headers } from \"next/headers\";\n\nimport { appRouter, type AppRouter } from \"@/server/api/root\";\nimport { createTRPCContext } from \"@/server/api/trpc\";\nimport { transformer } from \"./shared\";\n\n/**\n * This wraps the `createTRPCContext` helper and provides the required context for the tRPC API when\n * handling a tRPC call from a React Server Component.\n */\nconst createContext = cache(() => {\n  const heads = new Headers(headers());\n  heads.set(\"x-trpc-source\", \"rsc\");\n  return createTRPCContext({\n    headers: heads,\n  });\n});\n\nexport const api = createTRPCProxyClient<AppRouter>({\n  transformer,\n  links: [\n    loggerLink({\n      enabled: (op) =>\n        // process.env.NODE_ENV === \"development\" ||\n        op.direction === \"down\" && op.result instanceof Error,\n    }),\n    /**\n     * Custom RSC link that lets us invoke procedures without using http requests. Since Server\n     * Components always run on the server, we can just call the procedure as a function.\n     */\n    () =>\n      ({ op }) =>\n        observable((observer) => {\n          createContext()\n            .then((ctx) => {\n              return callProcedure({\n                procedures: appRouter._def.procedures,\n                path: op.path,\n                rawInput: op.input,\n                ctx,\n                type: op.type,\n              });\n            })\n            .then((data) => {\n              observer.next({ result: { data } });\n              observer.complete();\n            })\n            .catch((cause: TRPCErrorResponse) => {\n              observer.error(TRPCClientError.from(cause));\n            });\n        }),\n  ],\n});\n"
  },
  {
    "path": "src/trpc/shared.ts",
    "content": "import { type inferRouterInputs, type inferRouterOutputs } from \"@trpc/server\";\nimport superjson from \"superjson\";\n\nimport { type AppRouter } from \"@/server/api/root\";\n\nexport const transformer = superjson;\n\nfunction getBaseUrl() {\n  if (typeof window !== \"undefined\") return \"\";\n  if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;\n  return `http://localhost:${process.env.PORT ?? 3000}`;\n}\n\nexport function getUrl() {\n  return getBaseUrl() + \"/api/trpc\";\n}\n\n/**\n * Inference helper for inputs.\n *\n * @example type HelloInput = RouterInputs['example']['hello']\n */\nexport type RouterInputs = inferRouterInputs<AppRouter>;\n\n/**\n * Inference helper for outputs.\n *\n * @example type HelloOutput = RouterOutputs['example']['hello']\n */\nexport type RouterOutputs = inferRouterOutputs<AppRouter>;\n"
  },
  {
    "path": "tailwind.config.ts",
    "content": "import { type Config } from \"tailwindcss\";\nimport { fontFamily } from \"tailwindcss/defaultTheme\";\n\nexport default {\n  darkMode: [\"class\"],\n  content: [\"./src/**/*.{js,ts,jsx,tsx,mdx}\"],\n  theme: {\n    container: {\n      center: true,\n      padding: \"2rem\",\n      screens: {\n        \"2xl\": \"1400px\",\n      },\n    },\n    extend: {\n      fontFamily: {\n        sans: [\"var(--font-sans)\", ...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\n  plugins: [require(\"tailwindcss-animate\"), require(\"@tailwindcss/typography\")],\n} satisfies Config;\n"
  },
  {
    "path": "tests/e2e/auth-with-credential.spec.ts",
    "content": "import { db } from \"@/server/db\";\nimport { users } from \"@/server/db/schema\";\nimport { test, expect } from \"@playwright/test\";\nimport { eq } from \"drizzle-orm\";\nimport { extractLastCode, testUser } from \"./utils\";\nimport { readFileSync } from \"fs\";\n\ntest.beforeAll(() => {\n  db.delete(users)\n    .where(eq(users.email, testUser.email))\n    .catch((error) => {\n      console.error(error);\n    });\n});\n\ntest.describe(\"signup and login\", () => {\n  test(\"signup\", async ({ page }) => {\n    await page.goto(\"/\");\n    await page.getByText(\"login\").click();\n    await page.getByText(/sign up/i).click();\n    await page.waitForURL(\"/signup\");\n    await page.getByLabel(\"Email\").fill(testUser.email);\n    await page.getByLabel(\"Password\").fill(testUser.password);\n    await page.getByLabel(\"submit-btn\").click();\n    await page.waitForURL(\"/verify-email\");\n    const data = readFileSync(\"application.log\", { encoding: \"utf-8\" });\n    const code = extractLastCode(data);\n    expect(code).not.toBeNull();\n    await page.getByLabel(\"Verification Code\").fill(code!);\n    await page.getByLabel(\"submit-btn\").click();\n    await page.waitForURL(\"/dashboard\");\n  });\n  test(\"login and logout\", async ({ page }) => {\n    await page.goto(\"/\");\n    await page.getByText(\"login\").click();\n    await page.getByLabel(\"Email\").fill(testUser.email);\n    await page.getByLabel(\"Password\").fill(testUser.password);\n    await page.getByLabel(\"submit-btn\").click();\n    await page.waitForURL(\"/dashboard\");\n    await page.getByAltText(\"Avatar\").click();\n    await page.getByText(\"Sign out\").click();\n    await page.getByText(\"Continue\").click();\n    await page.waitForURL(\"/\");\n  });\n});\n"
  },
  {
    "path": "tests/e2e/utils.ts",
    "content": "export const testUser = {\n  name: \"Test User\",\n  email: \"test@saasykits.com\",\n  password: \"testPass123\",\n};\n\nexport function extractLastCode(log: string): string | null {\n  // Regular expression to match the code value\n  const regex = /\"code\":\"(\\d+)\"/g;\n\n  let match: RegExpExecArray | null;\n  let lastCode: string | null = null;\n\n  // Find all matches and keep track of the last one\n  while ((match = regex.exec(log)) !== null) {\n    lastCode = match[1] ?? null;\n  }\n  return lastCode;\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    /* Base Options: */\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true,\n    \"target\": \"es2022\",\n    \"allowJs\": true,\n    \"resolveJsonModule\": true,\n    \"moduleDetection\": \"force\",\n    \"isolatedModules\": true,\n\n    /* Strictness */\n    \"strict\": true,\n    \"noUncheckedIndexedAccess\": true,\n    \"checkJs\": true,\n\n    /* Bundled projects */\n    \"lib\": [\"dom\", \"dom.iterable\", \"ES2022\"],\n    \"noEmit\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Bundler\",\n    \"jsx\": \"preserve\",\n    \"plugins\": [{ \"name\": \"next\" }],\n    \"incremental\": true,\n\n    /* Path Aliases */\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"./src/*\"]\n    }\n  },\n  \"include\": [\n    \".eslintrc.cjs\",\n    \"next-env.d.ts\",\n    \"**/*.ts\",\n    \"**/*.tsx\",\n    \"**/*.cjs\",\n    \"**/*.js\",\n    \".next/types/**/*.ts\"\n  ],\n  \"exclude\": [\"node_modules\"]\n}\n"
  }
]