Full Code of iamtouha/next-lucia-auth for AI

main 6d0a4b31f322 cached
125 files
213.5 KB
65.3k tokens
123 symbols
1 requests
Download .txt
Showing preview only (243K chars total). Download the full file or copy to clipboard to get everything.
Repository: iamtouha/next-lucia-auth
Branch: main
Commit: 6d0a4b31f322
Files: 125
Total size: 213.5 KB

Directory structure:
gitextract_w3daecuw/

├── .devcontainer/
│   ├── Dockerfile
│   ├── compose.dev.yml
│   └── devcontainer.json
├── .eslintrc.cjs
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   ├── pull_request_template.md
│   └── workflows/
│       └── check.yaml
├── .gitignore
├── LICENSE
├── README.md
├── components.json
├── drizzle.config.ts
├── next.config.js
├── package.json
├── playwright.config.ts
├── postcss.config.cjs
├── prettier.config.js
├── src/
│   ├── app/
│   │   ├── (auth)/
│   │   │   ├── layout.tsx
│   │   │   ├── login/
│   │   │   │   ├── discord/
│   │   │   │   │   ├── callback/
│   │   │   │   │   │   └── route.ts
│   │   │   │   │   └── route.ts
│   │   │   │   ├── login.tsx
│   │   │   │   └── page.tsx
│   │   │   ├── reset-password/
│   │   │   │   ├── [token]/
│   │   │   │   │   ├── page.tsx
│   │   │   │   │   └── reset-password.tsx
│   │   │   │   ├── page.tsx
│   │   │   │   └── send-reset-email.tsx
│   │   │   ├── signup/
│   │   │   │   ├── page.tsx
│   │   │   │   └── signup.tsx
│   │   │   └── verify-email/
│   │   │       ├── page.tsx
│   │   │       └── verify-code.tsx
│   │   ├── (landing)/
│   │   │   ├── _components/
│   │   │   │   ├── copy-to-clipboard.tsx
│   │   │   │   ├── feature-icons.tsx
│   │   │   │   ├── footer.tsx
│   │   │   │   ├── header.tsx
│   │   │   │   └── hover-card.tsx
│   │   │   ├── layout.tsx
│   │   │   └── page.tsx
│   │   ├── (main)/
│   │   │   ├── _components/
│   │   │   │   ├── footer.tsx
│   │   │   │   ├── header.tsx
│   │   │   │   └── user-dropdown.tsx
│   │   │   ├── account/
│   │   │   │   └── page.tsx
│   │   │   ├── dashboard/
│   │   │   │   ├── _components/
│   │   │   │   │   ├── dashboard-nav.tsx
│   │   │   │   │   ├── new-post.tsx
│   │   │   │   │   ├── post-card-skeleton.tsx
│   │   │   │   │   ├── post-card.tsx
│   │   │   │   │   ├── posts-skeleton.tsx
│   │   │   │   │   ├── posts.tsx
│   │   │   │   │   └── verificiation-warning.tsx
│   │   │   │   ├── billing/
│   │   │   │   │   ├── _components/
│   │   │   │   │   │   ├── billing-skeleton.tsx
│   │   │   │   │   │   ├── billing.tsx
│   │   │   │   │   │   └── manage-subscription-form.tsx
│   │   │   │   │   └── page.tsx
│   │   │   │   ├── layout.tsx
│   │   │   │   ├── page.tsx
│   │   │   │   └── settings/
│   │   │   │       └── page.tsx
│   │   │   ├── editor/
│   │   │   │   └── [postId]/
│   │   │   │       ├── _components/
│   │   │   │       │   ├── post-editor.tsx
│   │   │   │       │   └── post-preview.tsx
│   │   │   │       └── page.tsx
│   │   │   └── layout.tsx
│   │   ├── api/
│   │   │   ├── trpc/
│   │   │   │   └── [trpc]/
│   │   │   │       └── route.ts
│   │   │   └── webhooks/
│   │   │       └── stripe/
│   │   │           └── route.ts
│   │   ├── icon.tsx
│   │   ├── layout.tsx
│   │   ├── robots.ts
│   │   └── sitemap.ts
│   ├── components/
│   │   ├── icons.tsx
│   │   ├── loading-button.tsx
│   │   ├── password-input.tsx
│   │   ├── responsive-dialog.tsx
│   │   ├── submit-button.tsx
│   │   ├── theme-provider.tsx
│   │   ├── theme-toggle.tsx
│   │   └── ui/
│   │       ├── alert-dialog.tsx
│   │       ├── alert.tsx
│   │       ├── badge.tsx
│   │       ├── button.tsx
│   │       ├── card.tsx
│   │       ├── dialog.tsx
│   │       ├── drawer.tsx
│   │       ├── dropdown-menu.tsx
│   │       ├── form.tsx
│   │       ├── input.tsx
│   │       ├── label.tsx
│   │       ├── pagination.tsx
│   │       ├── skeleton.tsx
│   │       ├── sonner.tsx
│   │       ├── tabs.tsx
│   │       └── textarea.tsx
│   ├── config/
│   │   └── subscriptions.ts
│   ├── env.js
│   ├── lib/
│   │   ├── auth/
│   │   │   ├── actions.ts
│   │   │   ├── index.ts
│   │   │   └── validate-request.ts
│   │   ├── constants.ts
│   │   ├── email/
│   │   │   ├── index.tsx
│   │   │   └── templates/
│   │   │       ├── email-verification.tsx
│   │   │       └── reset-password.tsx
│   │   ├── fonts.ts
│   │   ├── hooks/
│   │   │   ├── use-debounce.ts
│   │   │   └── use-media-query.ts
│   │   ├── logger.ts
│   │   ├── stripe.ts
│   │   ├── utils.ts
│   │   └── validators/
│   │       └── auth.ts
│   ├── middleware.ts
│   ├── server/
│   │   ├── api/
│   │   │   ├── root.ts
│   │   │   ├── routers/
│   │   │   │   ├── post/
│   │   │   │   │   ├── post.input.ts
│   │   │   │   │   ├── post.procedure.ts
│   │   │   │   │   └── post.service.ts
│   │   │   │   ├── stripe/
│   │   │   │   │   ├── stripe.input.ts
│   │   │   │   │   ├── stripe.procedure.ts
│   │   │   │   │   └── stripe.service.ts
│   │   │   │   └── user/
│   │   │   │       └── user.procedure.ts
│   │   │   └── trpc.ts
│   │   └── db/
│   │       ├── index.ts
│   │       └── schema.ts
│   ├── styles/
│   │   └── globals.css
│   └── trpc/
│       ├── react.tsx
│       ├── server.ts
│       └── shared.ts
├── tailwind.config.ts
├── tests/
│   └── e2e/
│       ├── auth-with-credential.spec.ts
│       └── utils.ts
└── tsconfig.json

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

================================================
FILE: .devcontainer/Dockerfile
================================================
FROM mcr.microsoft.com/devcontainers/javascript-node:20 


================================================
FILE: .devcontainer/compose.dev.yml
================================================
services:
  workspace:
    build: 
      dockerfile: Dockerfile
    volumes:
      - ../:/workspace:cached
    command: /bin/sh -c "while sleep 1000; do :; done"  
    depends_on:
      - database
      
  database:
    image: postgres:17.2-alpine
    environment:
      POSTGRES_DB: acme
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: root
 

================================================
FILE: .devcontainer/devcontainer.json
================================================
{
  "name": "Next.js Auth Template",
  "dockerComposeFile": ["compose.dev.yml"],
  "service": "workspace",
  "workspaceFolder": "/workspace",
  "postCreateCommand": "pnpm config set store-dir $HOME/.pnpm-store",
  "postStartCommand": "pnpm install",
  "forwardPorts": [3000],
  "features": {
    "ghcr.io/devcontainers-extra/features/pnpm": "latest"
  },
  "customizations": {
    "vscode": {
      "settings": {
        "editor.codeActionsOnSave": {
          "source.fixAll.eslint": "explicit",
          "source.organizeImports": "explicit",
          "source.removeUnusedImports": "explicit"
        },
        "editor.guides.bracketPairs": "active",
        "editor.rulers": [100],
        "typescript.tsdk": "node_modules/typescript/lib"
      },
      "extensions": [
        "dsznajder.es7-react-js-snippets",
        "eamodio.gitlens",
        "esbenp.prettier-vscode",
        "YoavBls.pretty-ts-errors",
        "bradlc.vscode-tailwindcss"
      ]
    }
  }
}


================================================
FILE: .eslintrc.cjs
================================================
/** @type {import("eslint").Linter.Config} */
const config = {
  parser: "@typescript-eslint/parser",
  parserOptions: {
    project: true,
  },
  plugins: ["@typescript-eslint"],
  extends: [
    "plugin:@next/next/recommended",
    "plugin:@typescript-eslint/recommended-type-checked",
    "plugin:@typescript-eslint/stylistic-type-checked",
  ],
  rules: {
    // These opinionated rules are enabled in stylistic-type-checked above.
    // Feel free to reconfigure them to your own preference.
    "@typescript-eslint/array-type": "off",
    "@typescript-eslint/consistent-type-definitions": "off",
    "@typescript-eslint/no-empty-interface": "off",

    "@typescript-eslint/consistent-type-imports": [
      "warn",
      {
        prefer: "type-imports",
        fixStyle: "inline-type-imports",
      },
    ],
    "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
    "@typescript-eslint/require-await": "off",
    "@typescript-eslint/no-misused-promises": [
      "error",
      {
        checksVoidReturn: { attributes: false },
      },
    ],
  },
  ignorePatterns: ["*.js"],
};

module.exports = config;


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''

---

**Describe the bug**
A clear and concise description of what the bug is.

**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error

**Expected behavior**
A clear and concise description of what you expected to happen.

**Screenshots**
If applicable, add screenshots to help explain your problem.

**Desktop (please complete the following information):**
 - OS: [e.g. iOS]
 - Browser [e.g. chrome, safari]
 - Version [e.g. 22]

**Smartphone (please complete the following information):**
 - Device: [e.g. iPhone6]
 - OS: [e.g. iOS8.1]
 - Browser [e.g. stock browser, safari]
 - Version [e.g. 22]

**Additional context**
Add any other context about the problem here.


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''

---

**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

**Describe the solution you'd like**
A clear and concise description of what you want to happen.

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

**Additional context**
Add any other context or screenshots about the feature request here.


================================================
FILE: .github/pull_request_template.md
================================================
# Description

Please 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.

Fixes # (issue)

## Type of change
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] This change requires a documentation update
- [ ] This change requires installing new dependencies



================================================
FILE: .github/workflows/check.yaml
================================================
name: Lint & Test

on:
  pull_request:
    branches: [main]

concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

permissions:
  contents: read

jobs:
  typecheck-and-lint:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2
      - uses: pnpm/action-setup@v2
        with:
          version: 9
      - uses: actions/setup-node@v3
        with:
          node-version: 20.x
          cache: "pnpm"

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Type Check and Lint
        run: pnpm run typecheck && pnpm run lint
        env:
          SKIP_ENV_VALIDATION: true

  e2e-test:
    needs: typecheck-and-lint
    timeout-minutes: 60
    runs-on: ubuntu-latest

    env:
      DATABASE_URL: ${{secrets.DATABASE_URL}}
      DISCORD_CLIENT_ID: ${{secrets.DISCORD_CLIENT_ID}}
      DISCORD_CLIENT_SECRET: ${{secrets.DISCORD_CLIENT_SECRET}}
      MOCK_SEND_EMAIL: "true"
      SMTP_HOST: host
      SMTP_PORT: 587
      SMTP_USER: user
      SMTP_PASSWORD: password
      NEXT_PUBLIC_APP_URL: http://localhost:3000
      STRIPE_API_KEY: stripe_api_key
      STRIPE_WEBHOOK_SECRET: stripe_webhook_secret
      STRIPE_PRO_MONTHLY_PLAN_ID: stripe_pro_monthly_plan_id

    steps:
      - uses: actions/checkout@v2
      - uses: pnpm/action-setup@v2
        with:
          version: 9
      - uses: actions/setup-node@v3
        with:
          node-version: 20.x
          cache: "pnpm"
      - name: Install dependencies
        run: pnpm install --frozen-lockfile
      - name: Build the app
        run: pnpm build
      - name: Install Playwright Browsers
        run: pnpm exec playwright install chromium --with-deps
      - name: Run Playwright tests
        run: pnpm exec playwright test
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30


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

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# database
/prisma/db.sqlite
/prisma/db.sqlite-journal

# next.js
/.next/
/out/
next-env.d.ts

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
application.log

# local env files
# 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
.env
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
tests/e2e/output

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

Copyright (c) [2023] [Touha Zohair]

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

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

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


================================================
FILE: README.md
================================================
# Next.js Auth Starter Template

## Motivation

Implementing 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.

## Lucia vs. NextAuth.js

Lucia 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.

## Key Features

- **Authentication:** 💼 Support for Credential and OAuth authentication.
- **Authorization:** 🔒 Easily manage public and protected routes within the `app directory`.
- **Email Verification:** 📧 Verify user identities through email.
- **Password Reset:** 🔑 Streamline password resets by sending email password reset links.
- **Lucia + tRPC:** 🔄 Similar to NextAuth with tRPC, granting access to sessions and user information through tRPC procedures.
- **E2E tests:** 🧪 Catch every issue before your users do with comprehensive E2E testing.
- **Stripe Payment:** 💳 Setup user subscriptions seamlessly with stripe.
- **Email template with react-email:** ✉️ Craft your email templates using React.
- **PostgreSQL Database:** 🛢️ Utilize a PostgreSQL database set up using Drizzle for enhanced performance and type safety.
- **Database Migration:** 🚀 Included migration script to extend the database schema according to your project needs.

## Tech Stack

- [Next.js](https://nextjs.org)
- [Lucia](https://lucia-auth.com/)
- [tRPC](https://trpc.io)
- [Drizzle ORM](https://orm.drizzle.team/)
- [PostgreSQL](https://www.postgresql.org/)
- [Stripe](https://stripe.com/)
- [Tailwind CSS](https://tailwindcss.com)
- [Shadcn UI](https://ui.shadcn.com/)
- [React Hook Form](https://www.react-hook-form.com/)
- [React Email](https://react.email/)
- [Playwright](https://playwright.dev/)

## Get Started

1. Clone this repository to your local machine.
2. Copy `.env.example` to `.env` and fill in the required environment variables.
3. Run `pnpm install` to install dependencies.
4. `(for node v18 or lower):` Uncomment polyfills for `webCrypto` in `src/lib/auth/index.ts`
5. Update app title, database prefix, and other parameters in the `src/lib/constants.ts` file.
6. Run `pnpm db:push` to push your schema to the database.
7. Execute `pnpm dev` to start the development server and enjoy!

## Testing

1. Install [Playwright](https://playwright.dev/) (use this command if you want to install chromium only `pnpm exec playwright install chromium --with-deps`)
2. Build production files using `pnpm build`
3. Run `pnpm test:e2e` (add --debug flag to open tests in browser in debug mode)

## Using Github actions

Add the following environment variables to your **github actions repository secrets** -
`DATABASE_URL`, `DISCORD_CLIENT_ID`, `DISCORD_CLIENT_SECRET`

## Roadmap

- [ ] Update Password
- [x] Stripe Integration
<!-- - [x] API Rate-Limiting see branch - [upstash-ratelimiting](https://github.com/iamtouha/next-lucia-auth/tree/upstash-ratelimiting) -->
- [ ] Admin Dashboard (under consideration)
- [ ] Role-Based Access Policy (under consideration)

## Contributing

To 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.


================================================
FILE: components.json
================================================
{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "default",
  "rsc": true,
  "tsx": true,
  "tailwind": {
    "config": "tailwind.config.ts",
    "css": "src/styles/globals.css",
    "baseColor": "zinc",
    "cssVariables": true
  },
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils"
  }
}


================================================
FILE: drizzle.config.ts
================================================
import { defineConfig } from "drizzle-kit";
import { DATABASE_PREFIX } from "@/lib/constants";

export default defineConfig({
  schema: "./src/server/db/schema.ts",
  out: "./drizzle",
  dialect: "postgresql",
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
  tablesFilter: [`${DATABASE_PREFIX}_*`],
});


================================================
FILE: next.config.js
================================================
await import("./src/env.js");

/** @type {import("next").NextConfig} */
const config = {};

export default config;


================================================
FILE: package.json
================================================
{
  "name": "next-lucia-auth",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "scripts": {
    "build": "next build",
    "db:push": "dotenv drizzle-kit push",
    "db:generate": "dotenv drizzle-kit generate",
    "db:migrate": "dotenv drizzle-kit migrate",
    "db:studio": "dotenv drizzle-kit studio",
    "dev": "next dev",
    "start": "next start",
    "lint": "next lint",
    "typecheck": "tsc --noEmit",
    "stripe:listen": "stripe listen --forward-to localhost:3000/api/webhooks/stripe --latest",
    "test:e2e": "playwright test"
  },
  "dependencies": {
    "@hookform/resolvers": "^3.9.0",
    "@lucia-auth/adapter-drizzle": "1.0.7",
    "@radix-ui/react-alert-dialog": "^1.1.1",
    "@radix-ui/react-dialog": "^1.1.1",
    "@radix-ui/react-dropdown-menu": "^2.1.1",
    "@radix-ui/react-icons": "^1.3.0",
    "@radix-ui/react-label": "^2.1.0",
    "@radix-ui/react-slot": "^1.1.0",
    "@radix-ui/react-tabs": "^1.1.0",
    "@react-email/components": "^0.0.12",
    "@react-email/render": "^0.0.10",
    "@t3-oss/env-nextjs": "^0.7.3",
    "@tanstack/react-query": "^4.36.1",
    "@trpc/client": "^10.45.2",
    "@trpc/next": "^10.45.2",
    "@trpc/react-query": "^10.45.2",
    "@trpc/server": "^10.45.2",
    "arctic": "^1.9.2",
    "class-variance-authority": "^0.7.0",
    "clsx": "^2.1.1",
    "lucia": "3.2.0",
    "next": "^14.2.5",
    "next-themes": "^0.2.1",
    "nodemailer": "^6.9.14",
    "oslo": "^1.2.1",
    "postgres": "^3.4.4",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "react-hook-form": "^7.52.1",
    "react-markdown": "^9.0.1",
    "react-syntax-highlighter": "^15.5.0",
    "rehype-raw": "^7.0.0",
    "remark-gfm": "^4.0.0",
    "server-only": "^0.0.1",
    "sonner": "^1.5.0",
    "stripe": "^14.25.0",
    "superjson": "^2.2.1",
    "tailwind-merge": "^2.4.0",
    "tailwindcss-animate": "^1.0.7",
    "vaul": "^0.8.9",
    "zod": "^3.23.8"
  },
  "devDependencies": {
    "@next/eslint-plugin-next": "^14.2.5",
    "@playwright/test": "^1.45.3",
    "@tailwindcss/typography": "^0.5.13",
    "@types/eslint": "^8.56.11",
    "@types/node": "^18.19.42",
    "@types/nodemailer": "^6.4.15",
    "@types/react": "^18.3.3",
    "@types/react-dom": "^18.3.0",
    "@types/react-syntax-highlighter": "^15.5.13",
    "@typescript-eslint/eslint-plugin": "^6.21.0",
    "@typescript-eslint/parser": "^6.21.0",
    "autoprefixer": "^10.4.19",
    "dotenv": "^16.4.5",
    "dotenv-cli": "^7.4.2",
    "drizzle-kit": "^0.23.0",
    "drizzle-orm": "^0.32.1",
    "eslint": "^8.57.0",
    "pg": "^8.12.0",
    "postcss": "^8.4.40",
    "prettier": "^3.3.3",
    "prettier-plugin-tailwindcss": "^0.5.14",
    "tailwindcss": "^3.4.7",
    "tsx": "^4.16.2",
    "typescript": "^5.5.4"
  },
  "ct3aMetadata": {
    "initVersion": "7.24.2"
  }
}


================================================
FILE: playwright.config.ts
================================================
import { defineConfig, devices } from "@playwright/test";
import "dotenv/config";

const baseURL = `http://localhost:${process.env.PORT ?? 3000}`;

export default defineConfig({
  testDir: "./tests/e2e",
  outputDir: "./tests/e2e/output",
  timeout: 60 * 1000,
  fullyParallel: true,

  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: "html",
  use: {
    trace: "on-first-retry",
    baseURL,
  },

  projects: [
    {
      name: "chromium",
      use: { ...devices["Desktop Chrome"] },
    },
  ],
  webServer: {
    command: "npx cross-env NODE_ENV=test npm run start",
    url: baseURL,
    stdout: "pipe",
    stderr: "pipe",
    reuseExistingServer: !process.env.CI,
  },
});


================================================
FILE: postcss.config.cjs
================================================
const config = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
};

module.exports = config;


================================================
FILE: prettier.config.js
================================================
/** @type {import('prettier').Config & import('prettier-plugin-tailwindcss').PluginOptions} */
const config = {
  plugins: ["prettier-plugin-tailwindcss"],
  tabWidth: 2,
  semi: true,
  singleQuote: false,
  printWidth: 100,
};

export default config;


================================================
FILE: src/app/(auth)/layout.tsx
================================================
import type { ReactNode } from "react";

const AuthLayout = ({ children }: { children: ReactNode }) => {
  return (
    <div className="grid min-h-screen place-items-center p-4">{children}</div>
  );
};

export default AuthLayout;


================================================
FILE: src/app/(auth)/login/discord/callback/route.ts
================================================
import { cookies } from "next/headers";
import { generateId } from "lucia";
import { OAuth2RequestError } from "arctic";
import { eq } from "drizzle-orm";
import { discord, lucia } from "@/lib/auth";
import { db } from "@/server/db";
import { Paths } from "@/lib/constants";
import { users } from "@/server/db/schema";

export async function GET(request: Request): Promise<Response> {
  const url = new URL(request.url);
  const code = url.searchParams.get("code");
  const state = url.searchParams.get("state");
  const storedState = cookies().get("discord_oauth_state")?.value ?? null;

  if (!code || !state || !storedState || state !== storedState) {
    return new Response(null, {
      status: 400,
      headers: { Location: Paths.Login },
    });
  }

  try {
    const tokens = await discord.validateAuthorizationCode(code);

    const discordUserRes = await fetch("https://discord.com/api/users/@me", {
      headers: {
        Authorization: `Bearer ${tokens.accessToken}`,
      },
    });
    const discordUser = (await discordUserRes.json()) as DiscordUser;

    if (!discordUser.email || !discordUser.verified) {
      return new Response(
        JSON.stringify({
          error: "Your Discord account must have a verified email address.",
        }),
        { status: 400, headers: { Location: Paths.Login } },
      );
    }
    const existingUser = await db.query.users.findFirst({
      where: (table, { eq, or }) =>
        or(eq(table.discordId, discordUser.id), eq(table.email, discordUser.email!)),
    });

    const avatar = discordUser.avatar
      ? `https://cdn.discordapp.com/avatars/${discordUser.id}/${discordUser.avatar}.webp`
      : null;

    if (!existingUser) {
      const userId = generateId(21);
      await db.insert(users).values({
        id: userId,
        email: discordUser.email,
        emailVerified: true,
        discordId: discordUser.id,
        avatar,
      });
      const session = await lucia.createSession(userId, {});
      const sessionCookie = lucia.createSessionCookie(session.id);
      cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
      return new Response(null, {
        status: 302,
        headers: { Location: Paths.Dashboard },
      });
    }

    if (existingUser.discordId !== discordUser.id || existingUser.avatar !== avatar) {
      await db
        .update(users)
        .set({
          discordId: discordUser.id,
          emailVerified: true,
          avatar,
        })
        .where(eq(users.id, existingUser.id));
    }
    const session = await lucia.createSession(existingUser.id, {});
    const sessionCookie = lucia.createSessionCookie(session.id);
    cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
    return new Response(null, {
      status: 302,
      headers: { Location: Paths.Dashboard },
    });
  } catch (e) {
    // the specific error message depends on the provider
    if (e instanceof OAuth2RequestError) {
      // invalid code
      return new Response(JSON.stringify({ message: "Invalid code" }), {
        status: 400,
      });
    }
    console.error(e);

    return new Response(JSON.stringify({ message: "internal server error" }), {
      status: 500,
    });
  }
}

interface DiscordUser {
  id: string;
  username: string;
  avatar: string | null;
  banner: string | null;
  global_name: string | null;
  banner_color: string | null;
  mfa_enabled: boolean;
  locale: string;
  email: string | null;
  verified: boolean;
}


================================================
FILE: src/app/(auth)/login/discord/route.ts
================================================
import { cookies } from "next/headers";
import { generateState } from "arctic";
import { discord } from "@/lib/auth";
import { env } from "@/env";

export async function GET(): Promise<Response> {
  const state = generateState();
  const url = await discord.createAuthorizationURL(state, {
    scopes: ["identify", "email"],
  });

  cookies().set("discord_oauth_state", state, {
    path: "/",
    secure: env.NODE_ENV === "production",
    httpOnly: true,
    maxAge: 60 * 10,
    sameSite: "lax",
  });

  return Response.redirect(url);
}


================================================
FILE: src/app/(auth)/login/login.tsx
================================================
"use client";

import Link from "next/link";
import { useFormState } from "react-dom";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { PasswordInput } from "@/components/password-input";
import { DiscordLogoIcon } from "@/components/icons";
import { APP_TITLE } from "@/lib/constants";
import { login } from "@/lib/auth/actions";
import { Label } from "@/components/ui/label";
import { SubmitButton } from "@/components/submit-button";

export function Login() {
  const [state, formAction] = useFormState(login, null);

  return (
    <Card className="w-full max-w-md">
      <CardHeader className="text-center">
        <CardTitle>{APP_TITLE} Log In</CardTitle>
        <CardDescription>Log in to your account to access your dashboard</CardDescription>
      </CardHeader>
      <CardContent>
        <Button variant="outline" className="w-full" asChild>
          <Link href="/login/discord" prefetch={false}>
            <DiscordLogoIcon className="mr-2 h-5 w-5" />
            Log in with Discord
          </Link>
        </Button>
        <div className="my-2 flex items-center">
          <div className="flex-grow border-t border-muted" />
          <div className="mx-2 text-muted-foreground">or</div>
          <div className="flex-grow border-t border-muted" />
        </div>
        <form action={formAction} className="grid gap-4">
          <div className="space-y-2">
            <Label htmlFor="email">Email</Label>
            <Input
              required
              id="email"
              placeholder="email@example.com"
              autoComplete="email"
              name="email"
              type="email"
            />
          </div>

          <div className="space-y-2">
            <Label htmlFor="password">Password</Label>
            <PasswordInput
              id="password"
              name="password"
              required
              autoComplete="current-password"
              placeholder="********"
            />
          </div>

          <div className="flex flex-wrap justify-between">
            <Button variant={"link"} size={"sm"} className="p-0" asChild>
              <Link href={"/signup"}>Not signed up? Sign up now.</Link>
            </Button>
            <Button variant={"link"} size={"sm"} className="p-0" asChild>
              <Link href={"/reset-password"}>Forgot password?</Link>
            </Button>
          </div>

          {state?.fieldError ? (
            <ul className="list-disc space-y-1 rounded-lg border bg-destructive/10 p-2 text-[0.8rem] font-medium text-destructive">
              {Object.values(state.fieldError).map((err) => (
                <li className="ml-4" key={err}>
                  {err}
                </li>
              ))}
            </ul>
          ) : state?.formError ? (
            <p className="rounded-lg border bg-destructive/10 p-2 text-[0.8rem] font-medium text-destructive">
              {state?.formError}
            </p>
          ) : null}
          <SubmitButton className="w-full" aria-label="submit-btn">
            Log In
          </SubmitButton>
          <Button variant="outline" className="w-full" asChild>
            <Link href="/">Cancel</Link>
          </Button>
        </form>
      </CardContent>
    </Card>
  );
}


================================================
FILE: src/app/(auth)/login/page.tsx
================================================
import { redirect } from "next/navigation";
import { validateRequest } from "@/lib/auth/validate-request";
import { Paths } from "@/lib/constants";
import { Login } from "./login";

export const metadata = {
  title: "Login",
  description: "Login Page",
};

export default async function LoginPage() {
  const { user } = await validateRequest();

  if (user) redirect(Paths.Dashboard);

  return <Login />;
}


================================================
FILE: src/app/(auth)/reset-password/[token]/page.tsx
================================================
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";
import { ResetPassword } from "./reset-password";

export const metadata = {
  title: "Reset Password",
  description: "Reset Password Page",
};

export default function ResetPasswordPage({
  params,
}: {
  params: { token: string };
}) {
  return (
    <Card className="w-full max-w-md">
      <CardHeader className="space-y-1">
        <CardTitle>Reset password</CardTitle>
        <CardDescription>Enter new password.</CardDescription>
      </CardHeader>
      <CardContent>
        <ResetPassword token={params.token} />
      </CardContent>
    </Card>
  );
}


================================================
FILE: src/app/(auth)/reset-password/[token]/reset-password.tsx
================================================
"use client";

import { useEffect } from "react";
import { useFormState } from "react-dom";
import { toast } from "sonner";
import { ExclamationTriangleIcon } from "@/components/icons";
import { SubmitButton } from "@/components/submit-button";
import { PasswordInput } from "@/components/password-input";
import { Label } from "@/components/ui/label";
import { resetPassword } from "@/lib/auth/actions";

export function ResetPassword({ token }: { token: string }) {
  const [state, formAction] = useFormState(resetPassword, null);

  useEffect(() => {
    if (state?.error) {
      toast(state.error, {
        icon: <ExclamationTriangleIcon className="h-5 w-5 text-destructive" />,
      });
    }
  }, [state?.error]);

  return (
    <form action={formAction} className="space-y-4">
      <input type="hidden" name="token" value={token} />
      <div className="space-y-2">
        <Label>New Password</Label>
        <PasswordInput
          name="password"
          required
          autoComplete="new-password"
          placeholder="********"
        />
      </div>
      <SubmitButton className="w-full">Reset Password</SubmitButton>
    </form>
  );
}


================================================
FILE: src/app/(auth)/reset-password/page.tsx
================================================
import { redirect } from "next/navigation";
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";
import { SendResetEmail } from "./send-reset-email";
import { validateRequest } from "@/lib/auth/validate-request";
import { Paths } from "@/lib/constants";

export const metadata = {
  title: "Forgot Password",
  description: "Forgot Password Page",
};

export default async function ForgotPasswordPage() {
  const { user } = await validateRequest();

  if (user) redirect(Paths.Dashboard);

  return (
    <Card className="w-full max-w-md">
      <CardHeader>
        <CardTitle>Forgot password?</CardTitle>
        <CardDescription>
          Password reset link will be sent to your email.
        </CardDescription>
      </CardHeader>
      <CardContent>
        <SendResetEmail />
      </CardContent>
    </Card>
  );
}


================================================
FILE: src/app/(auth)/reset-password/send-reset-email.tsx
================================================
"use client";

import { useEffect } from "react";
import { useFormState } from "react-dom";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { toast } from "sonner";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { SubmitButton } from "@/components/submit-button";
import { sendPasswordResetLink } from "@/lib/auth/actions";
import { ExclamationTriangleIcon } from "@/components/icons";
import { Paths } from "@/lib/constants";

export function SendResetEmail() {
  const [state, formAction] = useFormState(sendPasswordResetLink, null);
  const router = useRouter();

  useEffect(() => {
    if (state?.success) {
      toast("A password reset link has been sent to your email.");
      router.push(Paths.Login);
    }
    if (state?.error) {
      toast(state.error, {
        icon: <ExclamationTriangleIcon className="h-5 w-5 text-destructive" />,
      });
    }
  }, [state?.error, state?.success]);

  return (
    <form className="space-y-4" action={formAction}>
      <div className="space-y-2">
        <Label>Your Email</Label>
        <Input
          required
          placeholder="email@example.com"
          autoComplete="email"
          name="email"
          type="email"
        />
      </div>

      <div className="flex flex-wrap justify-between">
        <Link href={Paths.Signup}>
          <Button variant={"link"} size={"sm"} className="p-0">
            Not signed up? Sign up now
          </Button>
        </Link>
      </div>

      <SubmitButton className="w-full">Reset Password</SubmitButton>
      <Button variant="outline" className="w-full" asChild>
        <Link href="/">Cancel</Link>
      </Button>
    </form>
  );
}


================================================
FILE: src/app/(auth)/signup/page.tsx
================================================
import { redirect } from "next/navigation";
import { Signup } from "./signup";
import { validateRequest } from "@/lib/auth/validate-request";
import { Paths } from "@/lib/constants";

export const metadata = {
  title: "Sign Up",
  description: "Signup Page",
};

export default async function SignupPage() {
  const { user } = await validateRequest();

  if (user) redirect(Paths.Dashboard);

  return <Signup />;
}


================================================
FILE: src/app/(auth)/signup/signup.tsx
================================================
"use client";

import { useFormState } from "react-dom";
import Link from "next/link";
import { PasswordInput } from "@/components/password-input";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { DiscordLogoIcon } from "@/components/icons";
import { APP_TITLE } from "@/lib/constants";
import { Label } from "@/components/ui/label";
import { signup } from "@/lib/auth/actions";
import { SubmitButton } from "@/components/submit-button";

export function Signup() {
  const [state, formAction] = useFormState(signup, null);

  return (
    <Card className="w-full max-w-md">
      <CardHeader className="text-center">
        <CardTitle>{APP_TITLE} Sign Up</CardTitle>
        <CardDescription>Sign up to start using the app</CardDescription>
      </CardHeader>
      <CardContent>
        <Button variant="outline" className="w-full" asChild>
          <Link href="/login/discord" prefetch={false}>
            <DiscordLogoIcon className="mr-2 h-5 w-5" />
            Sign up with Discord
          </Link>
        </Button>
        <div className="my-2 flex items-center">
          <div className="flex-grow border-t border-muted" />
          <div className="mx-2 text-muted-foreground">or</div>
          <div className="flex-grow border-t border-muted" />
        </div>

        <form action={formAction} className="space-y-4">
          <div className="space-y-2">
            <Label htmlFor="email">Email</Label>
            <Input
              id="email"
              required
              placeholder="email@example.com"
              autoComplete="email"
              name="email"
              type="email"
            />
          </div>
          <div className="space-y-2">
            <Label htmlFor="password">Password</Label>
            <PasswordInput
              id="password"
              name="password"
              required
              autoComplete="current-password"
              placeholder="********"
            />
          </div>

          {state?.fieldError ? (
            <ul className="list-disc space-y-1 rounded-lg border bg-destructive/10 p-2 text-[0.8rem] font-medium text-destructive">
              {Object.values(state.fieldError).map((err) => (
                <li className="ml-4" key={err}>
                  {err}
                </li>
              ))}
            </ul>
          ) : state?.formError ? (
            <p className="rounded-lg border bg-destructive/10 p-2 text-[0.8rem] font-medium text-destructive">
              {state?.formError}
            </p>
          ) : null}
          <div>
            <Link href={"/login"}>
              <span className="p-0 text-xs font-medium underline-offset-4 hover:underline">
                Already signed up? Login instead.
              </span>
            </Link>
          </div>

          <SubmitButton className="w-full" aria-label="submit-btn">
            Sign Up
          </SubmitButton>
          <Button variant="outline" className="w-full" asChild>
            <Link href="/">Cancel</Link>
          </Button>
        </form>
      </CardContent>
    </Card>
  );
}


================================================
FILE: src/app/(auth)/verify-email/page.tsx
================================================
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";
import { redirect } from "next/navigation";
import { validateRequest } from "@/lib/auth/validate-request";
import { VerifyCode } from "./verify-code";
import { Paths } from "@/lib/constants";

export const metadata = {
  title: "Verify Email",
  description: "Verify Email Page",
};

export default async function VerifyEmailPage() {
  const { user } = await validateRequest();

  if (!user) redirect(Paths.Login);
  if (user.emailVerified) redirect(Paths.Dashboard);

  return (
    <Card className="w-full max-w-md">
      <CardHeader>
        <CardTitle>Verify Email</CardTitle>
        <CardDescription>
          Verification code was sent to <strong>{user.email}</strong>. Check
          your spam folder if you can't find the email.
        </CardDescription>
      </CardHeader>
      <CardContent>
        <VerifyCode />
      </CardContent>
    </Card>
  );
}


================================================
FILE: src/app/(auth)/verify-email/verify-code.tsx
================================================
"use client";
import { Input } from "@/components/ui/input";
import { Label } from "@radix-ui/react-label";
import { useEffect, useRef } from "react";
import { useFormState } from "react-dom";
import { toast } from "sonner";
import { ExclamationTriangleIcon } from "@/components/icons";
import { logout, verifyEmail, resendVerificationEmail as resendEmail } from "@/lib/auth/actions";
import { SubmitButton } from "@/components/submit-button";

export const VerifyCode = () => {
  const [verifyEmailState, verifyEmailAction] = useFormState(verifyEmail, null);
  const [resendState, resendAction] = useFormState(resendEmail, null);
  const codeFormRef = useRef<HTMLFormElement>(null);

  useEffect(() => {
    if (resendState?.success) {
      toast("Email sent!");
    }
    if (resendState?.error) {
      toast(resendState.error, {
        icon: <ExclamationTriangleIcon className="h-5 w-5 text-destructive" />,
      });
    }
  }, [resendState?.error, resendState?.success]);

  useEffect(() => {
    if (verifyEmailState?.error) {
      toast(verifyEmailState.error, {
        icon: <ExclamationTriangleIcon className="h-5 w-5 text-destructive" />,
      });
    }
  }, [verifyEmailState?.error]);

  return (
    <div className="flex flex-col gap-2">
      <form ref={codeFormRef} action={verifyEmailAction}>
        <Label htmlFor="code">Verification Code</Label>
        <Input className="mt-2" type="text" id="code" name="code" required />
        <SubmitButton className="mt-4 w-full" aria-label="submit-btn">
          Verify
        </SubmitButton>
      </form>
      <form action={resendAction}>
        <SubmitButton className="w-full" variant="secondary">
          Resend Code
        </SubmitButton>
      </form>
      <form action={logout}>
        <SubmitButton variant="link" className="p-0 font-normal">
          want to use another email? Log out now.
        </SubmitButton>
      </form>
    </div>
  );
};


================================================
FILE: src/app/(landing)/_components/copy-to-clipboard.tsx
================================================
"use client";

import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
import { CheckIcon, CopyIcon } from "@radix-ui/react-icons";
import { useState } from "react";
import { toast } from "sonner";

export const CopyToClipboard = ({ text }: { text: string }) => {
  const [copied, setCopied] = useState(false);
  const copyToClipboard = async () => {
    setCopied(true);
    setTimeout(() => {
      setCopied(false);
    }, 2000);
    await navigator.clipboard.writeText(text);
    toast("Copied to clipboard", {
      icon: <CopyIcon className="h-4 w-4" />,
    });
  };
  return (
    <div className="flex justify-center gap-3">
      <Input readOnly value={text} className="bg-secondary text-muted-foreground" />
      <Button size="icon" onClick={() => copyToClipboard()}>
        {copied ? (
          <CheckIcon
            className={cn(
              copied ? "opacity-100" : "opacity-0",
              "h-5 w-5 transition-opacity duration-500",
            )}
          />
        ) : (
          <CopyIcon className="h-5 w-5" />
        )}
      </Button>
    </div>
  );
};


================================================
FILE: src/app/(landing)/_components/feature-icons.tsx
================================================
import { forwardRef, type SVGProps } from "react";

import { cn } from "@/lib/utils";

const NextjsLight = forwardRef<SVGSVGElement, SVGProps<SVGSVGElement>>(
  ({ className, ...props }, ref) => (
    <svg
      ref={ref}
      {...props}
      viewBox="0 0 180 180"
      className={cn(className)}
      xmlns="http://www.w3.org/2000/svg"
    >
      <mask
        id="mask0_408_134"
        style={{ maskType: "alpha" }}
        maskUnits="userSpaceOnUse"
        x="0"
        y="0"
        width="180"
        height="180"
      >
        <circle cx="90" cy="90" r="90" fill="black" />
      </mask>
      <g mask="url(#mask0_408_134)">
        <circle cx="90" cy="90" r="90" fill="black" />
        <path
          d="M149.508 157.52L69.142 54H54V125.97H66.1136V69.3836L139.999 164.845C143.333 162.614 146.509 160.165 149.508 157.52Z"
          fill="url(#paint0_linear_408_134)"
        />
        <rect x="115" y="54" width="12" height="72" fill="url(#paint1_linear_408_134)" />
      </g>
      <defs>
        <linearGradient
          id="paint0_linear_408_134"
          x1="109"
          y1="116.5"
          x2="144.5"
          y2="160.5"
          gradientUnits="userSpaceOnUse"
        >
          <stop stopColor="white" />
          <stop offset="1" stopColor="white" stopOpacity="0" />
        </linearGradient>
        <linearGradient
          id="paint1_linear_408_134"
          x1="121"
          y1="54"
          x2="120.799"
          y2="106.875"
          gradientUnits="userSpaceOnUse"
        >
          <stop stopColor="white" />
          <stop offset="1" stopColor="white" stopOpacity="0" />
        </linearGradient>
      </defs>
    </svg>
  ),
);
NextjsLight.displayName = "NextjsLight";

const NextjsDark = forwardRef<SVGSVGElement, SVGProps<SVGSVGElement>>(
  ({ className, ...props }, ref) => (
    <svg
      ref={ref}
      {...props}
      viewBox="0 0 180 180"
      className={cn(className)}
      xmlns="http://www.w3.org/2000/svg"
    >
      <mask
        id="mask0_408_139"
        style={{ maskType: "alpha" }}
        maskUnits="userSpaceOnUse"
        x="0"
        y="0"
        width="180"
        height="180"
      >
        <circle cx="90" cy="90" r="90" fill="black" />
      </mask>
      <g mask="url(#mask0_408_139)">
        <circle cx="90" cy="90" r="87" fill="black" stroke="white" strokeWidth="6" />
        <path
          d="M149.508 157.52L69.142 54H54V125.97H66.1136V69.3836L139.999 164.845C143.333 162.614 146.509 160.165 149.508 157.52Z"
          fill="url(#paint0_linear_408_139)"
        />
        <rect x="115" y="54" width="12" height="72" fill="url(#paint1_linear_408_139)" />
      </g>
      <defs>
        <linearGradient
          id="paint0_linear_408_139"
          x1="109"
          y1="116.5"
          x2="144.5"
          y2="160.5"
          gradientUnits="userSpaceOnUse"
        >
          <stop stopColor="white" />
          <stop offset="1" stopColor="white" stopOpacity="0" />
        </linearGradient>
        <linearGradient
          id="paint1_linear_408_139"
          x1="121"
          y1="54"
          x2="120.799"
          y2="106.875"
          gradientUnits="userSpaceOnUse"
        >
          <stop stopColor="white" />
          <stop offset="1" stopColor="white" stopOpacity="0" />
        </linearGradient>
      </defs>
    </svg>
  ),
);
NextjsDark.displayName = "NextjsDark";

const ReactJs = forwardRef<SVGSVGElement, SVGProps<SVGSVGElement>>(
  ({ className, ...props }, ref) => (
    <svg
      ref={ref}
      {...props}
      viewBox="0 0 24 24"
      xmlns="http://www.w3.org/2000/svg"
      fill="currentColor"
      className={cn(className)}
    >
      <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>{" "}
    </svg>
  ),
);
ReactJs.displayName = "ReactJs";

const TailwindCss = forwardRef<SVGSVGElement, SVGProps<SVGSVGElement>>(
  ({ className, ...props }, ref) => (
    <svg
      ref={ref}
      xmlns="http://www.w3.org/2000/svg"
      {...props}
      className={cn(className)}
      fill="currentColor"
      viewBox="0 0 24 24"
    >
      <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>
    </svg>
  ),
);
TailwindCss.displayName = "TailwindCss";

const LuciaAuth = forwardRef<SVGSVGElement, SVGProps<SVGSVGElement>>(
  ({ className, ...props }, ref) => (
    <svg
      ref={ref}
      {...props}
      viewBox="0 0 2000 2000"
      xmlns="http://www.w3.org/2000/svg"
      fill="currentColor"
      className={cn(className)}
    >
      <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" />
    </svg>
  ),
);
LuciaAuth.displayName = "LuciaAuth";

const Drizzle = forwardRef<SVGSVGElement, SVGProps<SVGSVGElement>>(
  ({ className, ...props }, ref) => (
    <svg
      ref={ref}
      {...props}
      viewBox="0 0 160 160"
      xmlns="http://www.w3.org/2000/svg"
      fill="currentColor"
      className={cn(className)}
    >
      <rect
        width="9.63139"
        height="40.8516"
        rx="4.8157"
        transform="matrix(0.873028 0.48767 -0.497212 0.867629 43.4805 67.3037)"
        fill="currentColor"
      ></rect>
      <rect
        width="9.63139"
        height="40.8516"
        rx="4.8157"
        transform="matrix(0.873028 0.48767 -0.497212 0.867629 76.9395 46.5342)"
        fill="currentColor"
      ></rect>
      <rect
        width="9.63139"
        height="40.8516"
        rx="4.8157"
        transform="matrix(0.873028 0.48767 -0.497212 0.867629 128.424 46.5352)"
        fill="currentColor"
      ></rect>
      <rect
        width="9.63139"
        height="40.8516"
        rx="4.8157"
        transform="matrix(0.873028 0.48767 -0.497212 0.867629 94.957 67.3037)"
        fill="currentColor"
      ></rect>
    </svg>
  ),
);
Drizzle.displayName = "Drizzle";

const TRPC = forwardRef<SVGSVGElement, SVGProps<SVGSVGElement>>(({ className, ...props }, ref) => (
  <svg
    ref={ref}
    {...props}
    viewBox="0 0 512 512"
    xmlns="http://www.w3.org/2000/svg"
    fill="currentColor"
    className={cn(className)}
  >
    <g>
      <polygon points="246.2,162.3 202.9,137.3 202.9,187.4 246.2,212.4" />
      <polygon points="96.5,357.9 139.9,382.9 139.9,332.9 96.5,307.8" />
      <polygon points="149.1,266.8 105.7,291.9 149.1,316.9 192.4,291.9" />
      <polygon points="264.7,212.4 308,187.4 308,137.3 264.7,162.3" />
      <polygon points="298.8,121.3 255.4,96.3 212.2,121.3 255.4,146.4" />
      <polygon points="201.7,307.8 158.3,332.9 158.3,382.9 201.7,357.9" />
      <path
        d="M362,0H150C67.2,0,0,67.2,0,150v212c0,82.8,67.2,150,150,150h212c82.8,0,150-67.2,150-150V150
		C512,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
		l0,0V116l71-41l71.1,41v22.5l86,49.7v85l23.1,13.3V368.6z"
      />
      <polygon points="373.8,383 417.2,357.9 417.2,307.8 373.8,332.9" />
      <polygon points="364.6,266.9 321.3,291.9 364.6,317 407.9,291.9" />
      <polygon
        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 
		184.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 
		293.6,368.5"
      />
      <polygon points="312,358 355.4,383 355.4,332.9 312,307.9" />
    </g>
  </svg>
));
TRPC.displayName = "TRPC";

const ShadcnUi = forwardRef<SVGSVGElement, SVGProps<SVGSVGElement>>(
  ({ className, ...props }, ref) => (
    <svg
      ref={ref}
      {...props}
      viewBox="0 0 256 256"
      xmlns="http://www.w3.org/2000/svg"
      fill="currentColor"
      className={cn(className)}
    >
      <rect width="256" height="256" fill="none"></rect>
      <line
        x1="208"
        y1="128"
        x2="128"
        y2="208"
        fill="none"
        stroke="currentColor"
        strokeLinecap="round"
        strokeLinejoin="round"
        strokeWidth="16"
      ></line>
      <line
        x1="192"
        y1="40"
        x2="40"
        y2="192"
        fill="none"
        stroke="currentColor"
        strokeLinecap="round"
        strokeLinejoin="round"
        strokeWidth="16"
      ></line>
    </svg>
  ),
);
ShadcnUi.displayName = "ShadcnUi";

const ReactEmail = forwardRef<SVGSVGElement, SVGProps<SVGSVGElement>>(
  ({ className, ...props }, ref) => (
    <svg
      ref={ref}
      {...props}
      viewBox="0 0 32 32"
      xmlns="http://www.w3.org/2000/svg"
      className={cn(className)}
    >
      <g clipPath="url(#clip0_27_291)">
        <path
          fillRule="evenodd"
          clipRule="evenodd"
          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"
          fill="currentColor"
          stroke="currentColor"
          strokeWidth="0.5"
        ></path>
      </g>
      <path
        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"
        fill="currentColor"
      ></path>
      <path
        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"
        fill="currentColor"
      ></path>
      <path
        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"
        fill="currentColor"
      ></path>
      <path
        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"
        fill="currentColor"
      ></path>
      <path
        d="M69.8726 22.176V11.6H71.8406V22.176H69.8726ZM68.2086 15.568V13.744H73.6806V15.568H68.2086Z"
        fill="currentColor"
      ></path>
      <path
        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"
        fill="currentColor"
      ></path>
      <path
        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"
        fill="currentColor"
      ></path>
      <path
        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"
        fill="currentColor"
      ></path>
      <path
        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"
        fill="currentColor"
      ></path>
      <path d="M116.675 22.176V10.336H118.659V22.176H116.675Z" fill="currentColor"></path>
      <defs>
        <clipPath id="clip0_27_291">
          <rect width="32" height="32" rx="8" fill="currentColor"></rect>
        </clipPath>
      </defs>
    </svg>
  ),
);
ReactEmail.displayName = "ReactEmail";

const StripeLogo = forwardRef<SVGSVGElement, SVGProps<SVGSVGElement>>(
  ({ className, ...props }, ref) => (
    <svg
      ref={ref}
      {...props}
      viewBox="0 0 384 512"
      xmlns="http://www.w3.org/2000/svg"
      fill="currentColor"
      className={cn(className)}
    >
      <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" />
    </svg>
  ),
);
StripeLogo.displayName = "StripeLogo";

export {
  Drizzle,
  LuciaAuth,
  NextjsDark,
  NextjsLight,
  ReactEmail,
  ReactJs,
  ShadcnUi,
  StripeLogo,
  TailwindCss,
  TRPC,
};


================================================
FILE: src/app/(landing)/_components/footer.tsx
================================================
import { ThemeToggle } from "@/components/theme-toggle";
import { CodeIcon } from "@radix-ui/react-icons";

const githubUrl = "https://github.com/iamtouha/next-lucia-auth";
const twitterUrl = "https://twitter.com/iamtouha";

export const Footer = () => {
  return (
    <footer className="px-4 py-6">
      <div className="container flex items-center p-0">
        <CodeIcon className="mr-2 h-6 w-6" />
        <p className="text-sm">
          Built by{" "}
          <a className="underline underline-offset-4" href={twitterUrl}>
            iamtouha
          </a>
          . Get the source code from{" "}
          <a className="underline underline-offset-4" href={githubUrl}>
            GitHub
          </a>
          .
        </p>
        <div className="ml-auto">
          <ThemeToggle />
        </div>
      </div>
    </footer>
  );
};


================================================
FILE: src/app/(landing)/_components/header.tsx
================================================
import Link from "next/link";
import { RocketIcon } from "@/components/icons";
import { APP_TITLE } from "@/lib/constants";
import { Button } from "@/components/ui/button";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { HamburgerMenuIcon } from "@radix-ui/react-icons";

const routes = [
  { name: "Home", href: "/" },
  { name: "Features", href: "/#features" },
  {
    name: "Documentation",
    href: "https://www.touha.dev/posts/simple-nextjs-t3-authentication-with-lucia",
  },
] as const;

export const Header = () => {
  return (
    <header className="px-2 py-4 lg:py-6">
      <div className="container flex items-center gap-2 p-0">
        <DropdownMenu>
          <DropdownMenuTrigger asChild>
            <Button
              className="focus:outline-none focus:ring-1 md:hidden"
              size="icon"
              variant="outline"
            >
              <HamburgerMenuIcon className="h-5 w-5" />
            </Button>
          </DropdownMenuTrigger>
          <DropdownMenuContent align="start">
            <div className="py-1">
              {routes.map(({ name, href }) => (
                <DropdownMenuItem key={name} asChild>
                  <Link href={href}>{name}</Link>
                </DropdownMenuItem>
              ))}
            </div>
          </DropdownMenuContent>
        </DropdownMenu>
        <Link
          className="flex items-center justify-center text-xl font-medium"
          href="/"
        >
          <RocketIcon className="mr-2 h-5 w-5" /> {APP_TITLE}
        </Link>
        <nav className="ml-10 hidden gap-4 sm:gap-6 md:flex">
          {routes.map(({ name, href }) => (
            <Link
              key={name}
              className="text-sm font-medium text-muted-foreground/70 transition-colors hover:text-muted-foreground"
              href={href}
            >
              {name}
            </Link>
          ))}
        </nav>
        <div className="ml-auto">
          <Button asChild variant={"secondary"}>
            <Link href="/login">Login</Link>
          </Button>
        </div>
      </div>
    </header>
  );
};


================================================
FILE: src/app/(landing)/_components/hover-card.tsx
================================================
"use client";
import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import React, { useRef, useState } from "react";

type FeaturesProps = {
  name: string;
  description: string;
  logo: React.ReactNode;
};

const CardSpotlight = (props: FeaturesProps) => {
  const divRef = useRef<HTMLDivElement>(null);
  const [isFocused, setIsFocused] = useState(false);
  const [position, setPosition] = useState({ x: 0, y: 0 });
  const [opacity, setOpacity] = useState(0);

  const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
    if (!divRef.current || isFocused) return;

    const div = divRef.current;
    const rect = div.getBoundingClientRect();

    setPosition({ x: e.clientX - rect.left, y: e.clientY - rect.top });
  };

  const handleFocus = () => {
    setIsFocused(true);
    setOpacity(1);
  };

  const handleBlur = () => {
    setIsFocused(false);
    setOpacity(0);
  };

  const handleMouseEnter = () => {
    setOpacity(1);
  };

  const handleMouseLeave = () => {
    setOpacity(0);
  };

  return (
    <Card
      ref={divRef}
      onMouseMove={handleMouseMove}
      onFocus={handleFocus}
      onBlur={handleBlur}
      onMouseEnter={handleMouseEnter}
      onMouseLeave={handleMouseLeave}
      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"
    >
      <div
        className="pointer-events-none absolute -inset-px opacity-0 transition duration-300"
        style={{
          opacity,
          background: `radial-gradient(600px circle at ${position.x}px ${position.y}px, rgba(255,182,255,.1), transparent 40%)`,
        }}
      />
      <div className="pl-6 pt-6">{props.logo}</div>
      <CardHeader className="pb-6">
        <CardTitle className="text-xl">{props.name}</CardTitle>
        <CardDescription>{props.description}</CardDescription>
      </CardHeader>
    </Card>
  );
};

export default CardSpotlight;


================================================
FILE: src/app/(landing)/layout.tsx
================================================
import { APP_TITLE } from "@/lib/constants";
import { type Metadata } from "next";
import { type ReactNode } from "react";
import { Footer } from "./_components/footer";
import { Header } from "./_components/header";

export const metadata: Metadata = {
  title: APP_TITLE,
  description: "A Next.js starter with T3 stack and Lucia auth.",
};

function LandingPageLayout({ children }: { children: ReactNode }) {
  return (
    <>
      <Header />
      {children}
      <div className="h-20"></div>
      <Footer />
    </>
  );
}

export default LandingPageLayout;


================================================
FILE: src/app/(landing)/page.tsx
================================================
import { PlusIcon } from "@/components/icons";
import { Button } from "@/components/ui/button";
import { GitHubLogoIcon } from "@radix-ui/react-icons";
import { type Metadata } from "next";
import Link from "next/link";
import { CopyToClipboard } from "./_components/copy-to-clipboard";
import {
  Drizzle,
  LuciaAuth,
  NextjsDark,
  NextjsLight,
  ReactEmail,
  ReactJs,
  ShadcnUi,
  StripeLogo,
  TRPC,
  TailwindCss,
} from "./_components/feature-icons";
import CardSpotlight from "./_components/hover-card";

export const metadata: Metadata = {
  title: "Next.js Lucia Auth Starter Template",
  description:
    "A Next.js starter template with nextjs and Lucia auth. Includes drizzle, trpc, react-email, tailwindcss and shadcn-ui",
};

const githubUrl = "https://github.com/iamtouha/next-lucia-auth";

const features = [
  {
    name: "Next.js",
    description: "The React Framework for Production",
    logo: NextjsIcon,
  },
  {
    name: "React.js",
    description: "Server and client components.",
    logo: ReactJs,
  },
  {
    name: "Authentication",
    description: "Credential authentication with password reset and email validation",
    logo: LuciaAuth,
  },
  {
    name: "Database",
    description: "Drizzle with postgres database",
    logo: Drizzle,
  },
  {
    name: "TypeSafe Backend",
    description: "Preserve type safety from backend to frontend with tRPC",
    logo: TRPC,
  },
  {
    name: "Subscription",
    description: "Subscription with stripe",
    logo: StripeLogo,
  },
  {
    name: "Tailwindcss",
    description: "Simple and elegant UI components built with Tailwind CSS",
    logo: TailwindCss,
  },
  {
    name: "Shadcn UI",
    description: "A set of beautifully designed UI components for React",
    logo: ShadcnUi,
  },
  {
    name: "React Email",
    description: "Write emails in React with ease.",
    logo: ReactEmail,
  },
];

const HomePage = () => {
  return (
    <>
      <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">
        <div className="p-4">
          <div className="mb-10 flex items-center justify-center gap-3">
            <NextjsIcon className="h-[52px] w-[52px]" />
            <PlusIcon className="h-8 w-8" />
            <LuciaAuth className="h-14 w-14" />
          </div>
          <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">
            Next.js Lucia Auth Starter Template
          </h1>
          <p className="mb-10 mt-4 text-balance text-center text-muted-foreground md:text-lg lg:text-xl">
            A Next.js Authentication starter template (password reset, email validation and oAuth).
            Includes Lucia, Drizzle, tRPC, Stripe, tailwindcss, shadcn-ui and react-email.
          </p>
          <div className="mb-10">
            <div className="mx-auto max-w-[430px]">
              <CopyToClipboard text={"git clone " + githubUrl} />
            </div>
          </div>
          <div className="flex justify-center gap-4">
            <Button size="lg" variant="outline" asChild>
              <a href={githubUrl}>
                <GitHubLogoIcon className="mr-1 h-5 w-5" />
                GitHub
              </a>
            </Button>
            <Button size="lg" asChild>
              <Link href="/login">Get Started</Link>
            </Button>
          </div>
        </div>
      </section>
      <section>
        <div className="container mx-auto lg:max-w-screen-lg">
          <h1 className="mb-4 text-center text-3xl font-bold md:text-4xl lg:text-5xl">
            <a id="features"></a> Features
          </h1>
          <p className="mb-10 text-balance text-center text-muted-foreground md:text-lg lg:text-xl">
            This starter template is a guide to help you get started with Next.js for large scale
            applications. Feel free to add or remove features to suit your needs.
          </p>
          <div className="grid gap-6 sm:grid-cols-2 md:grid-cols-3">
            {features.map((feature, i) => (
              <CardSpotlight
                key={i}
                name={feature.name}
                description={feature.description}
                logo={<feature.logo className="h-12 w-12" />}
              />
            ))}
          </div>
        </div>
      </section>
    </>
  );
};

export default HomePage;

function NextjsIcon({ className }: { className?: string }) {
  return (
    <>
      <NextjsLight className={className + " dark:hidden"} />
      <NextjsDark className={className + " hidden dark:block"} />
    </>
  );
}


================================================
FILE: src/app/(main)/_components/footer.tsx
================================================
import { ThemeToggle } from "@/components/theme-toggle";
import { CodeIcon } from "@radix-ui/react-icons";

const githubUrl = "https://github.com/iamtouha/next-lucia-auth";
const twitterUrl = "https://twitter.com/iamtouha";

export const Footer = () => {
  return (
    <footer className="mt-6 px-4 py-6">
      <div className="container flex items-center p-0">
        <CodeIcon className="mr-2 h-6 w-6" />
        <p className="text-sm">
          Built by{" "}
          <a className="underline underline-offset-4" href={twitterUrl}>
            iamtouha
          </a>
          . Get the source code from{" "}
          <a className="underline underline-offset-4" href={githubUrl}>
            GitHub
          </a>
          .
        </p>
        <div className="ml-auto">
          <ThemeToggle />
        </div>
      </div>
    </footer>
  );
};


================================================
FILE: src/app/(main)/_components/header.tsx
================================================
import { UserDropdown } from "@/app/(main)/_components/user-dropdown";
import { RocketIcon } from "@/components/icons";
import { validateRequest } from "@/lib/auth/validate-request";
import { APP_TITLE } from "@/lib/constants";
import Link from "next/link";

export const Header = async () => {
  const { user } = await validateRequest();

  return (
    <header className="sticky top-0 z-10 border-b bg-background/80 p-0">
      <div className="container flex items-center gap-2 px-2 py-2 lg:px-4">
        <Link className="flex items-center justify-center text-xl font-medium" href="/">
          <RocketIcon className="mr-2 h-5 w-5" /> {APP_TITLE} Dashboard
        </Link>
        {user ? <UserDropdown email={user.email} avatar={user.avatar} className="ml-auto" /> : null}
      </div>
    </header>
  );
};


================================================
FILE: src/app/(main)/_components/user-dropdown.tsx
================================================
"use client";

import { ExclamationTriangleIcon } from "@/components/icons";
import { LoadingButton } from "@/components/loading-button";
import {
  AlertDialog,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { logout } from "@/lib/auth/actions";
import { APP_TITLE } from "@/lib/constants";
import Link from "next/link";
import { useState } from "react";
import { toast } from "sonner";

export const UserDropdown = ({
  email,
  avatar,
  className,
}: {
  email: string;
  avatar?: string | null;
  className?: string;
}) => {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger className={className}>
        {/* eslint @next/next/no-img-element:off */}
        <img
          src={avatar ?? "https://source.boringavatars.com/marble/60/" + email}
          alt="Avatar"
          className="block h-8 w-8 rounded-full leading-none"
          width={64}
          height={64}
        ></img>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end">
        <DropdownMenuLabel className="text-muted-foreground">{email}</DropdownMenuLabel>
        <DropdownMenuSeparator />
        <DropdownMenuGroup>
          <DropdownMenuItem className="cursor-pointer text-muted-foreground" asChild>
            <Link href="/dashboard">Dashboard</Link>
          </DropdownMenuItem>
          <DropdownMenuItem className="cursor-pointer text-muted-foreground" asChild>
            <Link href="/dashboard/billing">Billing</Link>
          </DropdownMenuItem>
          <DropdownMenuItem className="cursor-pointer text-muted-foreground" asChild>
            <Link href="/dashboard/settings">Settings</Link>
          </DropdownMenuItem>
        </DropdownMenuGroup>
        <DropdownMenuSeparator />

        <DropdownMenuLabel className="p-0">
          <SignoutConfirmation />
        </DropdownMenuLabel>
      </DropdownMenuContent>
    </DropdownMenu>
  );
};

const SignoutConfirmation = () => {
  const [open, setOpen] = useState(false);
  const [isLoading, setIsLoading] = useState(false);

  const handleSignout = async () => {
    setIsLoading(true);
    try {
      await logout();
      toast("Signed out successfully");
    } catch (error) {
      if (error instanceof Error) {
        toast(error.message, {
          icon: <ExclamationTriangleIcon className="h-4 w-4 text-destructive" />,
        });
      }
    } finally {
      setOpen(false);
      setIsLoading(false);
    }
  };

  return (
    <AlertDialog open={open} onOpenChange={setOpen}>
      <AlertDialogTrigger
        className="px-2 py-1.5 text-sm text-muted-foreground outline-none"
        asChild
      >
        <button>Sign out</button>
      </AlertDialogTrigger>
      <AlertDialogContent className="max-w-xs">
        <AlertDialogHeader>
          <AlertDialogTitle className="text-center">Sign out from {APP_TITLE}?</AlertDialogTitle>
          <AlertDialogDescription>You will be redirected to the home page.</AlertDialogDescription>
        </AlertDialogHeader>
        <div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-center">
          <Button variant="outline" onClick={() => setOpen(false)}>
            Cancel
          </Button>
          <LoadingButton loading={isLoading} onClick={handleSignout}>
            Continue
          </LoadingButton>
        </div>
      </AlertDialogContent>
    </AlertDialog>
  );
};


================================================
FILE: src/app/(main)/account/page.tsx
================================================
import { SubmitButton } from "@/components/submit-button";
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";
import { logout } from "@/lib/auth/actions";
import { validateRequest } from "@/lib/auth/validate-request";
import { Paths } from "@/lib/constants";
import { redirect } from "next/navigation";

export default async function AccountPage() {
  const { user } = await validateRequest();
  if (!user) redirect(Paths.Login);

  return (
    <main className="container mx-auto min-h-screen p-4">
      <Card className="max-w-sm">
        <CardHeader>
          <CardTitle> {user.email}!</CardTitle>
          <CardDescription>You've successfully logged in!</CardDescription>
        </CardHeader>
        <CardContent>This is a private page.</CardContent>
        <CardFooter>
          <form action={logout}>
            <SubmitButton variant="outline">Logout</SubmitButton>
          </form>
        </CardFooter>
      </Card>
    </main>
  );
}


================================================
FILE: src/app/(main)/dashboard/_components/dashboard-nav.tsx
================================================
"use client";

import { CreditCard, FileTextIcon, GearIcon } from "@/components/icons";
import Link from "next/link";
import { usePathname } from "next/navigation";

import { cn } from "@/lib/utils";

const items = [
  {
    title: "Posts",
    href: "/dashboard",
    icon: FileTextIcon,
  },

  {
    title: "Billing",
    href: "/dashboard/billing",
    icon: CreditCard,
  },
  {
    title: "Settings",
    href: "/dashboard/settings",
    icon: GearIcon,
  },
];

interface Props {
  className?: string;
}

export function DashboardNav({ className }: Props) {
  const path = usePathname();

  return (
    <nav className={cn(className)}>
      {items.map((item) => (
        <Link href={item.href} key={item.href}>
          <span
            className={cn(
              "group flex items-center rounded-md px-3 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground",
              path === item.href ? "bg-accent" : "transparent",
            )}
          >
            <item.icon className="mr-2 h-4 w-4" />
            <span>{item.title}</span>
          </span>
        </Link>
      ))}
    </nav>
  );
}


================================================
FILE: src/app/(main)/dashboard/_components/new-post.tsx
================================================
"use client";

import { FilePlusIcon } from "@/components/icons";
import { Button } from "@/components/ui/button";
import { api } from "@/trpc/react";
import { type RouterOutputs } from "@/trpc/shared";
import { useRouter } from "next/navigation";
import * as React from "react";
import { toast } from "sonner";
interface NewPostProps {
  isEligible: boolean;
  setOptimisticPosts: (action: {
    action: "add" | "delete" | "update";
    post: RouterOutputs["post"]["myPosts"][number];
  }) => void;
}

export const NewPost = ({ isEligible, setOptimisticPosts }: NewPostProps) => {
  const router = useRouter();
  const post = api.post.create.useMutation();
  const [isCreatePending, startCreateTransaction] = React.useTransition();

  const createPost = () => {
    if (!isEligible) {
      toast.message("You've reached the limit of posts for your current plan", {
        description: "Upgrade to create more posts",
      });
      return;
    }

    startCreateTransaction(async () => {
      await post.mutateAsync(
        {
          title: "Untitled Post",
          content: "Write your content here",
          excerpt: "untitled post",
        },
        {
          onSettled: () => {
            setOptimisticPosts({
              action: "add",
              post: {
                id: crypto.randomUUID(),
                title: "Untitled Post",
                excerpt: "untitled post",
                status: "draft",
                createdAt: new Date(),
              },
            });
          },
          onSuccess: ({ id }) => {
            toast.success("Post created");
            router.refresh();
            // This is a workaround for a bug in navigation because of router.refresh()
            setTimeout(() => {
              router.push(`/editor/${id}`);
            }, 100);
          },
          onError: () => {
            toast.error("Failed to create post");
          },
        },
      );
    });
  };

  return (
    <Button
      onClick={createPost}
      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"
      disabled={isCreatePending}
    >
      <div className="flex flex-col items-center gap-4">
        <FilePlusIcon className="h-10 w-10" />
        <p className="text-sm">New Post</p>
      </div>
    </Button>
  );
};


================================================
FILE: src/app/(main)/dashboard/_components/post-card-skeleton.tsx
================================================
import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";

export function PostCardSkeleton() {
  return (
    <Card>
      <CardHeader>
        <Skeleton className="h-7 w-24" />
        <Skeleton className="h-3 w-36" />
      </CardHeader>
      <CardContent className="line-clamp-3 text-sm">
        <Skeleton className="h-5 w-24" />
      </CardContent>
      <CardFooter className="justify-between gap-2">
        <Skeleton className="h-6 w-16" />
        <div className="flex items-center space-x-2">
          <Skeleton className="h-8 w-8" />
          <Skeleton className="h-8 w-12" />
        </div>
      </CardFooter>
    </Card>
  );
}


================================================
FILE: src/app/(main)/dashboard/_components/post-card.tsx
================================================
"use client";

import { Pencil2Icon, TrashIcon } from "@/components/icons";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";
import { api } from "@/trpc/react";
import { type RouterOutputs } from "@/trpc/shared";
import Link from "next/link";
import { useRouter } from "next/navigation";
import * as React from "react";
import { toast } from "sonner";

interface PostCardProps {
  post: RouterOutputs["post"]["myPosts"][number];
  userName?: string;
  setOptimisticPosts: (action: {
    action: "add" | "delete" | "update";
    post: RouterOutputs["post"]["myPosts"][number];
  }) => void;
}

export const PostCard = ({ post, userName, setOptimisticPosts }: PostCardProps) => {
  const router = useRouter();
  const postMutation = api.post.delete.useMutation();
  const [isDeletePending, startDeleteTransition] = React.useTransition();

  return (
    <Card>
      <CardHeader>
        <CardTitle className="line-clamp-2 text-base">{post.title}</CardTitle>
        <CardDescription className="line-clamp-1 text-sm">
          {userName ? <span>{userName} at</span> : null}
          {new Date(post.createdAt.toJSON()).toLocaleString(undefined, {
            dateStyle: "medium",
            timeStyle: "short",
          })}
        </CardDescription>
      </CardHeader>
      <CardContent className="line-clamp-3 text-sm">{post.excerpt}</CardContent>
      <CardFooter className="flex-row-reverse gap-2">
        <Button variant="secondary" size="sm" asChild>
          <Link href={`/editor/${post.id}`}>
            <Pencil2Icon className="mr-1 h-4 w-4" />
            <span>Edit</span>
          </Link>
        </Button>
        <Button
          variant="secondary"
          size="icon"
          className="h-8 w-8 text-destructive"
          onClick={() => {
            startDeleteTransition(async () => {
              await postMutation.mutateAsync(
                { id: post.id },
                {
                  onSettled: () => {
                    setOptimisticPosts({
                      action: "delete",
                      post,
                    });
                  },
                  onSuccess: () => {
                    toast.success("Post deleted");
                    router.refresh();
                  },
                  onError: () => {
                    toast.error("Failed to delete post");
                  },
                },
              );
            });
          }}
          disabled={isDeletePending}
        >
          <TrashIcon className="h-5 w-5" />
          <span className="sr-only">Delete</span>
        </Button>
        <Badge variant="outline" className="mr-auto rounded-lg capitalize">
          {post.status} Post
        </Badge>
      </CardFooter>
    </Card>
  );
};


================================================
FILE: src/app/(main)/dashboard/_components/posts-skeleton.tsx
================================================
import { PostCardSkeleton } from "./post-card-skeleton";

export function PostsSkeleton() {
  return (
    <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
      {Array.from({ length: 3 }).map((_, i) => (
        <PostCardSkeleton key={i} />
      ))}
    </div>
  );
}


================================================
FILE: src/app/(main)/dashboard/_components/posts.tsx
================================================
"use client";

import { type RouterOutputs } from "@/trpc/shared";
import * as React from "react";
import { NewPost } from "./new-post";
import { PostCard } from "./post-card";

interface PostsProps {
  promises: Promise<[RouterOutputs["post"]["myPosts"], RouterOutputs["stripe"]["getPlan"]]>;
}

export function Posts({ promises }: PostsProps) {
  /**
   * use is a React Hook that lets you read the value of a resource like a Promise or context.
   * @see https://react.dev/reference/react/use
   */
  const [posts, subscriptionPlan] = React.use(promises);

  /**
   * useOptimistic is a React Hook that lets you show a different state while an async action is underway.
   * 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.
   * @see https://react.dev/reference/react/useOptimistic
   */
  const [optimisticPosts, setOptimisticPosts] = React.useOptimistic(
    posts,
    (
      state,
      {
        action,
        post,
      }: {
        action: "add" | "delete" | "update";
        post: RouterOutputs["post"]["myPosts"][number];
      },
    ) => {
      switch (action) {
        case "delete":
          return state.filter((p) => p.id !== post.id);
        case "update":
          return state.map((p) => (p.id === post.id ? post : p));
        default:
          return [...state, post];
      }
    },
  );

  return (
    <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
      <NewPost
        isEligible={(optimisticPosts.length < 2 || subscriptionPlan?.isPro) ?? false}
        setOptimisticPosts={setOptimisticPosts}
      />
      {optimisticPosts.map((post) => (
        <PostCard key={post.id} post={post} setOptimisticPosts={setOptimisticPosts} />
      ))}
    </div>
  );
}


================================================
FILE: src/app/(main)/dashboard/_components/verificiation-warning.tsx
================================================
import { ExclamationTriangleIcon } from "@/components/icons";

import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { validateRequest } from "@/lib/auth/validate-request";
import Link from "next/link";

export async function VerificiationWarning() {
  const { user } = await validateRequest();

  return user?.emailVerified === false ? (
    <Alert className="rounded-lg bg-yellow-50 text-yellow-700 dark:bg-gray-800 dark:text-yellow-400">
      <ExclamationTriangleIcon className="h-5 w-5 !text-yellow-700 dark:!text-yellow-400" />
      <div className="flex lg:items-center">
        <div className="w-full">
          <AlertTitle>Account verification required</AlertTitle>
          <AlertDescription>
            A verification email has been sent to your email address. Please verify your account to
            access all features.
          </AlertDescription>
        </div>
        <Button size="sm" asChild>
          <Link href="/verify-email">Verify Email</Link>
        </Button>
      </div>
    </Alert>
  ) : null;
}


================================================
FILE: src/app/(main)/dashboard/billing/_components/billing-skeleton.tsx
================================================
import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";

export function BillingSkeleton() {
  return (
    <>
      <section>
        <Card className="space-y-2 p-8">
          <Skeleton className="h-7 w-24" />
          <Skeleton className="h-5 w-36" />
        </Card>
      </section>
      <section className="grid gap-6 lg:grid-cols-2">
        {Array.from({ length: 2 }).map((_, i) => (
          <Card key={i} className="flex flex-col p-2">
            <CardHeader className="h-full">
              <Skeleton className="h-7 w-24" />
              <Skeleton className="h-4 w-36" />
            </CardHeader>
            <CardContent className="h-full flex-1 space-y-6">
              <Skeleton className="h-8 w-24" />
              <div className="space-y-2">
                {Array.from({ length: 2 }).map((_, i) => (
                  <div key={i} className="flex items-center gap-2">
                    <Skeleton className="h-5 w-5 rounded-full" />
                    <Skeleton className="h-4 w-24" />
                  </div>
                ))}
              </div>
            </CardContent>
            <CardFooter className="pt-4">
              <Skeleton className="h-10 w-full" />
            </CardFooter>
          </Card>
        ))}
      </section>
    </>
  );
}


================================================
FILE: src/app/(main)/dashboard/billing/_components/billing.tsx
================================================
import Link from "next/link";

import { CheckIcon } from "@/components/icons";

import { Button } from "@/components/ui/button";
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";
import { formatDate } from "@/lib/utils";
import { type RouterOutputs } from "@/trpc/shared";
import { ManageSubscriptionForm } from "./manage-subscription-form";

interface BillingProps {
  stripePromises: Promise<
    [RouterOutputs["stripe"]["getPlans"], RouterOutputs["stripe"]["getPlan"]]
  >;
}

export async function Billing({ stripePromises }: BillingProps) {
  const [plans, plan] = await stripePromises;

  return (
    <>
      <section>
        <Card className="space-y-2 p-8">
          <h3 className="text-lg font-semibold sm:text-xl">{plan?.name ?? "Free"} plan</h3>
          <p className="text-sm text-muted-foreground">
            {!plan?.isPro
              ? "The free plan is limited to 2 posts. Upgrade to the Pro plan to unlock unlimited posts."
              : plan.isCanceled
                ? "Your plan will be canceled on "
                : "Your plan renews on "}
            {plan?.stripeCurrentPeriodEnd ? formatDate(plan.stripeCurrentPeriodEnd) : null}
          </p>
        </Card>
      </section>
      <section className="grid gap-6 lg:grid-cols-2">
        {plans.map((item) => (
          <Card key={item.name} className="flex flex-col p-2">
            <CardHeader className="h-full">
              <CardTitle className="line-clamp-1">{item.name}</CardTitle>
              <CardDescription className="line-clamp-2">{item.description}</CardDescription>
            </CardHeader>
            <CardContent className="h-full flex-1 space-y-6">
              <div className="text-3xl font-bold">
                {item.price}
                <span className="text-sm font-normal text-muted-foreground">/month</span>
              </div>
              <div className="space-y-2">
                {item.features.map((feature) => (
                  <div key={feature} className="flex items-center gap-2">
                    <div className="aspect-square shrink-0 rounded-full bg-foreground p-px text-background">
                      <CheckIcon className="size-4" aria-hidden="true" />
                    </div>
                    <span className="text-sm text-muted-foreground">{feature}</span>
                  </div>
                ))}
              </div>
            </CardContent>
            <CardFooter className="pt-4">
              {item.name === "Free" ? (
                <Button className="w-full" asChild>
                  <Link href="/dashboard">
                    Get started
                    <span className="sr-only">Get started</span>
                  </Link>
                </Button>
              ) : (
                <ManageSubscriptionForm
                  stripePriceId={item.stripePriceId}
                  isPro={plan?.isPro ?? false}
                  stripeCustomerId={plan?.stripeCustomerId}
                  stripeSubscriptionId={plan?.stripeSubscriptionId}
                />
              )}
            </CardFooter>
          </Card>
        ))}
      </section>
    </>
  );
}


================================================
FILE: src/app/(main)/dashboard/billing/_components/manage-subscription-form.tsx
================================================
"use client";

import * as React from "react";

import { Button } from "@/components/ui/button";
import type { ManageSubscriptionInput } from "@/server/api/routers/stripe/stripe.input";
import { api } from "@/trpc/react";
import { toast } from "sonner";

export function ManageSubscriptionForm({
  isPro,
  stripeCustomerId,
  stripeSubscriptionId,
  stripePriceId,
}: ManageSubscriptionInput) {
  const [isPending, startTransition] = React.useTransition();
  const managePlanMutation = api.stripe.managePlan.useMutation();

  function onSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();

    startTransition(async () => {
      try {
        const session = await managePlanMutation.mutateAsync({
          isPro,
          stripeCustomerId,
          stripeSubscriptionId,
          stripePriceId,
        });

        if (session) {
          window.location.href = session.url ?? "/dashboard/billing";
        }
      } catch (err) {
        err instanceof Error
          ? toast.error(err.message)
          : toast.error("An error occurred. Please try again.");
      }
    });
  }

  return (
    <form className="w-full" onSubmit={onSubmit}>
      <Button className="w-full" disabled={isPending}>
        {isPending ? "Loading..." : isPro ? "Manage plan" : "Subscribe now"}
      </Button>
    </form>
  );
}


================================================
FILE: src/app/(main)/dashboard/billing/page.tsx
================================================
import type { Metadata } from "next";
import { redirect } from "next/navigation";

import { ExclamationTriangleIcon } from "@/components/icons";

import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { env } from "@/env";
import { validateRequest } from "@/lib/auth/validate-request";
import { APP_TITLE } from "@/lib/constants";
import { api } from "@/trpc/server";
import * as React from "react";
import { Billing } from "./_components/billing";
import { BillingSkeleton } from "./_components/billing-skeleton";

export const metadata: Metadata = {
  metadataBase: new URL(env.NEXT_PUBLIC_APP_URL),
  title: "Billing",
  description: "Manage your billing and subscription",
};

export default async function BillingPage() {
  const { user } = await validateRequest();

  if (!user) {
    redirect("/signin");
  }

  const stripePromises = Promise.all([api.stripe.getPlans.query(), api.stripe.getPlan.query()]);

  return (
    <div className="grid gap-8">
      <div>
        <h1 className="text-3xl font-bold md:text-4xl">Billing</h1>
        <p className="text-sm text-muted-foreground">Manage your billing and subscription</p>
      </div>
      <section>
        <Alert className="p-6 [&>svg]:left-6 [&>svg]:top-6 [&>svg~*]:pl-10">
          <ExclamationTriangleIcon className="h-6 w-6" />
          <AlertTitle>This is a demo app.</AlertTitle>
          <AlertDescription>
            {APP_TITLE} app is a demo app using a Stripe test environment. You can find a list of
            test card numbers on the{" "}
            <a
              href="https://stripe.com/docs/testing#cards"
              target="_blank"
              rel="noreferrer"
              className="font-medium underline underline-offset-4"
            >
              Stripe docs
            </a>
            .
          </AlertDescription>
        </Alert>
      </section>
      <React.Suspense fallback={<BillingSkeleton />}>
        <Billing stripePromises={stripePromises} />
      </React.Suspense>
    </div>
  );
}


================================================
FILE: src/app/(main)/dashboard/layout.tsx
================================================
import { DashboardNav } from "./_components/dashboard-nav";
import { VerificiationWarning } from "./_components/verificiation-warning";

interface Props {
  children: React.ReactNode;
}

export default function DashboardLayout({ children }: Props) {
  return (
    <div className="container min-h-[calc(100vh-180px)] px-2 pt-6 md:px-4">
      <div className="flex flex-col gap-6 md:flex-row lg:gap-10">
        <DashboardNav className="flex flex-shrink-0 gap-2 md:w-48 md:flex-col lg:w-80" />
        <main className="w-full space-y-4">
          <VerificiationWarning />
          <div>{children}</div>
        </main>
      </div>
    </div>
  );
}


================================================
FILE: src/app/(main)/dashboard/page.tsx
================================================
import { env } from "@/env";
import { validateRequest } from "@/lib/auth/validate-request";
import { Paths } from "@/lib/constants";
import { myPostsSchema } from "@/server/api/routers/post/post.input";
import { api } from "@/trpc/server";
import { type Metadata } from "next";
import { redirect } from "next/navigation";
import * as React from "react";
import { Posts } from "./_components/posts";
import { PostsSkeleton } from "./_components/posts-skeleton";

export const metadata: Metadata = {
  metadataBase: new URL(env.NEXT_PUBLIC_APP_URL),
  title: "Posts",
  description: "Manage your posts here",
};

interface Props {
  searchParams: Record<string, string | string[] | undefined>;
}

export default async function DashboardPage({ searchParams }: Props) {
  const { page, perPage } = myPostsSchema.parse(searchParams);

  const { user } = await validateRequest();
  if (!user) redirect(Paths.Login);

  /**
   * Passing multiple promises to `Promise.all` to fetch data in parallel to prevent waterfall requests.
   * Passing promises to the `Posts` component to make them hot promises (they can run without being awaited) to prevent waterfall requests.
   * @see https://www.youtube.com/shorts/A7GGjutZxrs
   * @see https://nextjs.org/docs/app/building-your-application/data-fetching/patterns#parallel-data-fetching
   */
  const promises = Promise.all([
    api.post.myPosts.query({ page, perPage }),
    api.stripe.getPlan.query(),
  ]);

  return (
    <div>
      <div className="mb-6">
        <h1 className="text-3xl font-bold md:text-4xl">Posts</h1>
        <p className="text-sm text-muted-foreground">Manage your posts here</p>
      </div>
      <React.Suspense fallback={<PostsSkeleton />}>
        <Posts promises={promises} />
      </React.Suspense>
    </div>
  );
}


================================================
FILE: src/app/(main)/dashboard/settings/page.tsx
================================================
import type { Metadata } from "next";
import { redirect } from "next/navigation";

import { env } from "@/env";
import { validateRequest } from "@/lib/auth/validate-request";

export const metadata: Metadata = {
  metadataBase: new URL(env.NEXT_PUBLIC_APP_URL),
  title: "Billing",
  description: "Manage your billing and subscription",
};

export default async function BillingPage() {
  const { user } = await validateRequest();

  if (!user) {
    redirect("/signin");
  }

  return (
    <div className="grid gap-8">
      <div>
        <h1 className="text-3xl font-bold md:text-4xl">Settings</h1>
        <p className="text-sm text-muted-foreground">Manage your account settings</p>
      </div>
      <p>Work in progress...</p>
    </div>
  );
}


================================================
FILE: src/app/(main)/editor/[postId]/_components/post-editor.tsx
================================================
"use client";
import { useRef } from "react";
import { type RouterOutputs } from "@/trpc/shared";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import {
  Form,
  FormControl,
  FormDescription,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { PostPreview } from "./post-preview";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { api } from "@/trpc/react";
import { Pencil2Icon } from "@/components/icons";
import { LoadingButton } from "@/components/loading-button";
import Link from "next/link";
import { createPostSchema } from "@/server/api/routers/post/post.input";

const markdownlink = "https://remarkjs.github.io/react-markdown/";

interface Props {
  post: RouterOutputs["post"]["get"];
}

export const PostEditor = ({ post }: Props) => {
  if (!post) return null;
  const formRef = useRef<HTMLFormElement>(null);
  const updatePost = api.post.update.useMutation();
  const form = useForm({
    defaultValues: {
      title: post.title,
      excerpt: post.excerpt,
      content: post.content,
    },
    resolver: zodResolver(createPostSchema),
  });
  const onSubmit = form.handleSubmit(async (values) => {
    updatePost.mutate({ id: post.id, ...values });
  });

  return (
    <>
      <div className="flex items-center gap-2">
        <Pencil2Icon className="h-5 w-5" />
        <h1 className="text-2xl font-bold">{post.title}</h1>

        <LoadingButton
          disabled={!form.formState.isDirty}
          loading={updatePost.isLoading}
          onClick={() => formRef.current?.requestSubmit()}
          className="ml-auto"
        >
          Save
        </LoadingButton>
      </div>
      <div className="h-6"></div>
      <Form {...form}>
        <form ref={formRef} onSubmit={onSubmit} className="block max-w-screen-md space-y-4">
          <FormField
            control={form.control}
            name="title"
            render={({ field }) => (
              <FormItem>
                <FormLabel>Post Title</FormLabel>
                <FormControl>
                  <Input {...field} />
                </FormControl>

                <FormMessage />
              </FormItem>
            )}
          />
          <FormField
            control={form.control}
            name="excerpt"
            render={({ field }) => (
              <FormItem>
                <FormLabel>Excerpt</FormLabel>
                <FormControl>
                  <Textarea {...field} rows={2} className="min-h-0" />
                </FormControl>
                <FormDescription>A short description of your post</FormDescription>
                <FormMessage />
              </FormItem>
            )}
          />
          <FormField
            control={form.control}
            name="content"
            render={({ field }) => (
              <Tabs defaultValue="code">
                <TabsList>
                  <TabsTrigger value="code">Code</TabsTrigger>
                  <TabsTrigger value="preview">Preview</TabsTrigger>
                </TabsList>
                <TabsContent value="code">
                  <FormItem>
                    <FormControl>
                      <Textarea {...field} className="min-h-[200px]" />
                    </FormControl>
                    <FormMessage />
                  </FormItem>
                </TabsContent>
                <TabsContent value="preview" className="space-y-2">
                  <div className="prose prose-sm min-h-[200px] max-w-[none] rounded-lg border px-3 py-2 dark:prose-invert">
                    <PostPreview text={form.watch("content") || post.content} />
                  </div>
                </TabsContent>
                <Link href={markdownlink}>
                  <span className="text-[0.8rem] text-muted-foreground underline underline-offset-4">
                    Supports markdown
                  </span>
                </Link>
              </Tabs>
            )}
          />
        </form>
      </Form>
    </>
  );
};


================================================
FILE: src/app/(main)/editor/[postId]/_components/post-preview.tsx
================================================
import Markdown, { type Components } from "react-markdown";
import remarkGfm from "remark-gfm";
import rehypeRaw from "rehype-raw";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { materialOceanic } from "react-syntax-highlighter/dist/cjs/styles/prism";

const options: Components = {
  code: (props) => (
    <SyntaxHighlighter
      language={props.className?.replace(/(?:lang(?:uage)?-)/, "")}
      style={materialOceanic}
      wrapLines={true}
      className="not-prose rounded-md"
    >
      {String(props.children)}
    </SyntaxHighlighter>
  ),
};

export const PostPreview = ({ text }: { text: string }) => {
  return (
    <Markdown
      remarkPlugins={[remarkGfm]}
      rehypePlugins={[rehypeRaw]}
      components={options}
    >
      {text}
    </Markdown>
  );
};


================================================
FILE: src/app/(main)/editor/[postId]/page.tsx
================================================
import React from "react";
import { api } from "@/trpc/server";
import { notFound, redirect } from "next/navigation";
import { PostEditor } from "./_components/post-editor";
import { ArrowLeftIcon } from "@/components/icons";
import Link from "next/link";
import { validateRequest } from "@/lib/auth/validate-request";
import { Paths } from "@/lib/constants";

interface Props {
  params: {
    postId: string;
  };
}

export default async function EditPostPage({ params }: Props) {
  const { user } = await validateRequest();
  if (!user) redirect(Paths.Login);

  const post = await api.post.get.query({ id: params.postId });
  if (!post) notFound();

  return (
    <main className="container min-h-[calc(100vh-160px)] pt-3 md:max-w-screen-md">
      <Link
        href="/dashboard"
        className="mb-3 flex items-center gap-2 text-sm text-muted-foreground hover:underline"
      >
        <ArrowLeftIcon className="h-5 w-5" /> back to dashboard
      </Link>

      <PostEditor post={post} />
    </main>
  );
}


================================================
FILE: src/app/(main)/layout.tsx
================================================
import { type ReactNode } from "react";
import { Header } from "./_components/header";
import { Footer } from "./_components/footer";

const MainLayout = ({ children }: { children: ReactNode }) => {
  return (
    <>
      <Header />
      {children}
      <Footer />
    </>
  );
};

export default MainLayout;


================================================
FILE: src/app/api/trpc/[trpc]/route.ts
================================================
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { type NextRequest } from "next/server";

import { env } from "@/env";
import { appRouter } from "@/server/api/root";
import { createTRPCContext } from "@/server/api/trpc";

/**
 * This wraps the `createTRPCContext` helper and provides the required context for the tRPC API when
 * handling a HTTP request (e.g. when you make requests from Client Components).
 */
const createContext = async (req: NextRequest) => {
  return createTRPCContext({ headers: req.headers });
};

const handler = (req: NextRequest) =>
  fetchRequestHandler({
    endpoint: "/api/trpc",
    req,
    router: appRouter,
    createContext: () => createContext(req),
    onError:
      env.NODE_ENV === "development"
        ? ({ path, error }) => {
            console.error(
              `❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`,
            );
          }
        : undefined,
  });

export { handler as GET, handler as POST };


================================================
FILE: src/app/api/webhooks/stripe/route.ts
================================================
import { headers } from "next/headers";

import type Stripe from "stripe";

import { env } from "@/env";
import { stripe } from "@/lib/stripe";
import { db } from "@/server/db";
import { users } from "@/server/db/schema";
import { eq } from "drizzle-orm";

export async function POST(req: Request) {
  const body = await req.text();
  const signature = headers().get("Stripe-Signature") ?? "";

  let event: Stripe.Event;

  try {
    event = stripe.webhooks.constructEvent(
      body,
      signature,
      env.STRIPE_WEBHOOK_SECRET,
    );
  } catch (err) {
    return new Response(
      `Webhook Error: ${err instanceof Error ? err.message : "Unknown error."}`,
      { status: 400 },
    );
  }

  switch (event.type) {
    case "checkout.session.completed": {
      const checkoutSessionCompleted = event.data.object;

      const userId = checkoutSessionCompleted?.metadata?.userId;

      if (!userId) {
        return new Response("User id not found in checkout session metadata.", {
          status: 404,
        });
      }

      // Retrieve the subscription details from Stripe
      const subscription = await stripe.subscriptions.retrieve(
        checkoutSessionCompleted.subscription as string,
      );

      // Update the user stripe into in our database
      // Since this is the initial subscription, we need to update
      // the subscription id and customer id
      await db
        .update(users)
        .set({
          stripeSubscriptionId: subscription.id,
          stripeCustomerId: subscription.customer as string,
          stripePriceId: subscription.items.data[0]?.price.id,
          stripeCurrentPeriodEnd: new Date(
            subscription.current_period_end * 1000,
          ),
        })
        .where(eq(users.id, userId));

      break;
    }
    case "invoice.payment_succeeded": {
      const invoicePaymentSucceeded = event.data.object;

      const userId = invoicePaymentSucceeded?.metadata?.userId;

      if (!userId) {
        return new Response("User id not found in invoice metadata.", {
          status: 404,
        });
      }

      // Retrieve the subscription details from Stripe
      const subscription = await stripe.subscriptions.retrieve(
        invoicePaymentSucceeded.subscription as string,
      );

      // Update the price id and set the new period end
      await db
        .update(users)
        .set({
          stripePriceId: subscription.items.data[0]?.price.id,
          stripeCurrentPeriodEnd: new Date(
            subscription.current_period_end * 1000,
          ),
        })
        .where(eq(users.id, userId));

      break;
    }
    default:
      console.warn(`Unhandled event type: ${event.type}`);
  }

  return new Response(null, { status: 200 });
}


================================================
FILE: src/app/icon.tsx
================================================
import { ImageResponse } from "next/og";

// Route segment config
export const runtime = "edge";

// Image metadata
export const size = {
  width: 32,
  height: 32,
};
export const contentType = "image/png";

// Image generation
export default function Icon() {
  return new ImageResponse(
    (
      // ImageResponse JSX element
      <div
        tw="flex items-center justify-center bg-black text-[24px] leading-8 text-white"
        style={{
          width: 32,
          height: 32,
        }}
      >
        <svg
          width="24"
          height="24"
          viewBox="0 0 15 15"
          fill="none"
          xmlns="http://www.w3.org/2000/svg"
        >
          <path
            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"
            fill="currentColor"
            fill-rule="evenodd"
            clip-rule="evenodd"
          ></path>
        </svg>
      </div>
    ),
    // ImageResponse options
    {
      // For convenience, we can re-use the exported icons size metadata
      // config to also set the ImageResponse's width and height.
      ...size,
    },
  );
}


================================================
FILE: src/app/layout.tsx
================================================
import "@/styles/globals.css";

import { ThemeProvider } from "@/components/theme-provider";
import { Toaster } from "@/components/ui/sonner";
import { APP_TITLE } from "@/lib/constants";
import { fontSans } from "@/lib/fonts";
import { cn } from "@/lib/utils";
import { TRPCReactProvider } from "@/trpc/react";
import type { Metadata, Viewport } from "next";

export const metadata: Metadata = {
  title: {
    default: APP_TITLE,
    template: `%s | ${APP_TITLE}`,
  },
  description: "Acme - Simple auth with lucia and trpc",
  icons: [{ rel: "icon", url: "/icon.png" }],
};

export const viewport: Viewport = {
  themeColor: [
    { media: "(prefers-color-scheme: light)", color: "white" },
    { media: "(prefers-color-scheme: dark)", color: "black" },
  ],
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body
        className={cn(
          "min-h-screen bg-background font-sans antialiased",
          fontSans.variable,
        )}
      >
        <ThemeProvider
          attribute="class"
          defaultTheme="system"
          enableSystem
          disableTransitionOnChange
        >
          <TRPCReactProvider>{children}</TRPCReactProvider>
          <Toaster />
        </ThemeProvider>
      </body>
    </html>
  );
}


================================================
FILE: src/app/robots.ts
================================================
import { type MetadataRoute } from "next"

import { absoluteUrl } from "@/lib/utils"

export default function robots(): MetadataRoute.Robots {
  return {
    rules: {
      userAgent: "*",
      allow: "/",
    },
    sitemap: absoluteUrl("/sitemap.xml"),
  }
}


================================================
FILE: src/app/sitemap.ts
================================================
import { type MetadataRoute } from "next";

import { absoluteUrl } from "@/lib/utils";

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const routes = ["", "/dashboard", "/dashboard/billing"].map((route) => ({
    url: absoluteUrl(route),
    lastModified: new Date().toISOString(),
  }));

  return [...routes];
}


================================================
FILE: src/components/icons.tsx
================================================
import { forwardRef, type SVGProps } from "react";
import { cn } from "@/lib/utils";

const AnimatedSpinner = forwardRef<SVGSVGElement, SVGProps<SVGSVGElement>>(
  ({ className, ...props }, ref) => (
    <svg
      ref={ref}
      {...props}
      viewBox="0 0 24 24"
      xmlns="http://www.w3.org/2000/svg"
      fill="currentColor"
      className={cn(className)}
    >
      <g className="animated-spinner">
        <rect x="11" y="1" width="2" height="5" opacity=".14" />
        <rect
          x="11"
          y="1"
          width="2"
          height="5"
          transform="rotate(30 12 12)"
          opacity=".29"
        />
        <rect
          x="11"
          y="1"
          width="2"
          height="5"
          transform="rotate(60 12 12)"
          opacity=".43"
        />
        <rect
          x="11"
          y="1"
          width="2"
          height="5"
          transform="rotate(90 12 12)"
          opacity=".57"
        />
        <rect
          x="11"
          y="1"
          width="2"
          height="5"
          transform="rotate(120 12 12)"
          opacity=".71"
        />
        <rect
          x="11"
          y="1"
          width="2"
          height="5"
          transform="rotate(150 12 12)"
          opacity=".86"
        />
        <rect x="11" y="1" width="2" height="5" transform="rotate(180 12 12)" />
      </g>
    </svg>
  ),
);
AnimatedSpinner.displayName = "AnimatedSpinner";

const CreditCard = forwardRef<SVGSVGElement, SVGProps<SVGSVGElement>>(
  ({ className, ...props }, ref) => (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      ref={ref}
      {...props}
      viewBox="0 0 24 24"
      className={cn(className)}
      fill="none"
      stroke="currentColor"
      strokeWidth="2"
      strokeLinecap="round"
      strokeLinejoin="round"
    >
      <rect x="2" y="5" width="20" height="14" rx="2"></rect>
      <line x1="2" y1="10" x2="22" y2="10"></line>
    </svg>
  ),
);
CreditCard.displayName = "CreditCard";

export { AnimatedSpinner, CreditCard };

export {
  EyeOpenIcon,
  EyeNoneIcon as EyeCloseIcon,
  SunIcon,
  MoonIcon,
  ExclamationTriangleIcon,
  ExitIcon,
  EnterIcon,
  GearIcon,
  RocketIcon,
  PlusIcon,
  HamburgerMenuIcon,
  Pencil2Icon,
  UpdateIcon,
  CheckCircledIcon,
  PlayIcon,
  TrashIcon,
  ArchiveIcon,
  ResetIcon,
  DiscordLogoIcon,
  FileTextIcon,
  IdCardIcon,
  PlusCircledIcon,
  FilePlusIcon,
  CheckIcon,
  ChevronLeftIcon,
  ChevronRightIcon,
  DotsHorizontalIcon,
  ArrowLeftIcon,
} from "@radix-ui/react-icons";


================================================
FILE: src/components/loading-button.tsx
================================================
"use client";

import { forwardRef } from "react";
import { AnimatedSpinner } from "@/components/icons";
import { Button, type ButtonProps } from "@/components/ui/button";

import { cn } from "@/lib/utils";

export interface LoadingButtonProps extends ButtonProps {
  loading?: boolean;
}

const LoadingButton = forwardRef<HTMLButtonElement, LoadingButtonProps>(
  ({ loading = false, className, children, ...props }, ref) => {
    return (
      <Button
        ref={ref}
        {...props}
        disabled={props.disabled ? props.disabled : loading}
        className={cn(className, "relative")}
      >
        <span className={cn(loading ? "opacity-0" : "")}>{children}</span>
        {loading ? (
          <div className="absolute inset-0 grid place-items-center">
            <AnimatedSpinner className="h-6 w-6" />
          </div>
        ) : null}
      </Button>
    );
  },
);

LoadingButton.displayName = "LoadingButton";

export { LoadingButton };


================================================
FILE: src/components/password-input.tsx
================================================
"use client";

import * as React from "react";
import { EyeOpenIcon, EyeCloseIcon } from "@/components/icons";
import { Button } from "@/components/ui/button";
import { Input, type InputProps } from "@/components/ui/input";

import { cn } from "@/lib/utils";

const PasswordInputComponent = React.forwardRef<HTMLInputElement, InputProps>(
  ({ className, ...props }, ref) => {
    const [showPassword, setShowPassword] = React.useState(false);

    return (
      <div className="relative">
        <Input
          type={showPassword ? "text" : "password"}
          className={cn("pr-10", className)}
          ref={ref}
          {...props}
        />
        <Button
          type="button"
          variant="ghost"
          size="sm"
          className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
          onClick={() => setShowPassword((prev) => !prev)}
          disabled={props.value === "" || props.disabled}
        >
          {showPassword ? (
            <EyeCloseIcon className="h-4 w-4" aria-hidden="true" />
          ) : (
            <EyeOpenIcon className="h-4 w-4" aria-hidden="true" />
          )}
          <span className="sr-only">
            {showPassword ? "Hide password" : "Show password"}
          </span>
        </Button>
      </div>
    );
  },
);
PasswordInputComponent.displayName = "PasswordInput";

export const PasswordInput = PasswordInputComponent;


================================================
FILE: src/components/responsive-dialog.tsx
================================================
"use client";

import {
  useState,
  type ReactNode,
  type Dispatch,
  type SetStateAction,
} from "react";
import { Button } from "@/components/ui/button";
import {
  Dialog,
  DialogTitle,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTrigger,
  DialogFooter,
} from "@/components/ui/dialog";
import {
  Drawer,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
} from "@/components/ui/drawer";
import { useMediaQuery } from "@/lib/hooks/use-media-query";
import { cn } from "@/lib/utils";

type StatefulContent = ({
  open,
  setOpen,
}: {
  open: boolean;
  setOpen: Dispatch<SetStateAction<boolean>>;
}) => ReactNode | ReactNode[];

export const ResponsiveDialog = (props: {
  trigger: ReactNode;
  title?: ReactNode;
  description?: ReactNode;
  children: ReactNode | ReactNode[] | StatefulContent;
  footer?: ReactNode;
  contentClassName?: string;
}) => {
  const [open, setOpen] = useState(false);
  const isDesktop = useMediaQuery("(min-width: 640px)");

  return isDesktop ? (
    <Dialog open={open} onOpenChange={setOpen}>
      <DialogTrigger asChild>{props.trigger}</DialogTrigger>
      <DialogContent className={cn("max-w-md", props.contentClassName)}>
        <DialogHeader>
          <DialogTitle>{props.title}</DialogTitle>
          <DialogDescription>{props.description}</DialogDescription>
        </DialogHeader>
        {isFunctionType(props.children)
          ? props.children({ open, setOpen })
          : props.children}
      </DialogContent>
      {props.footer ? <DialogFooter>{props.footer}</DialogFooter> : null}
    </Dialog>
  ) : (
    <Drawer open={open} onOpenChange={setOpen}>
      <DrawerTrigger asChild>{props.trigger}</DrawerTrigger>
      <DrawerContent>
        <DrawerHeader className="text-left">
          <DrawerTitle>{props.title}</DrawerTitle>
          <DrawerDescription>{props.description}</DrawerDescription>
        </DrawerHeader>
        <div className={cn("px-4", props.contentClassName)}>
          {isFunctionType(props.children)
            ? props.children({ open, setOpen })
            : props.children}
        </div>
        <DrawerFooter className="pt-2">
          {props.footer ? (
            props.footer
          ) : (
            <DrawerClose asChild>
              <Button variant="outline">Cancel</Button>
            </DrawerClose>
          )}
        </DrawerFooter>
      </DrawerContent>
    </Drawer>
  );
};

const isFunctionType = (
  prop: ReactNode | ReactNode[] | StatefulContent,
): prop is ({
  open,
  setOpen,
}: {
  open: boolean;
  setOpen: Dispatch<SetStateAction<boolean>>;
}) => ReactNode | ReactNode[] => {
  return typeof prop === "function";
};


================================================
FILE: src/components/submit-button.tsx
================================================
"use client";

import { forwardRef } from "react";
import { useFormStatus } from "react-dom";
import { LoadingButton } from "@/components/loading-button";
import type { ButtonProps } from "@/components/ui/button";

const SubmitButton = forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, children, ...props }, ref) => {
    const { pending } = useFormStatus();
    return (
      <LoadingButton
        ref={ref}
        {...props}
        loading={pending}
        className={className}
      >
        {children}
      </LoadingButton>
    );
  },
);
SubmitButton.displayName = "SubmitButton";

export { SubmitButton };


================================================
FILE: src/components/theme-provider.tsx
================================================
"use client";

import * as React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import { type ThemeProviderProps } from "next-themes/dist/types";

export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
  return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}


================================================
FILE: src/components/theme-toggle.tsx
================================================
"use client";

import { useTheme } from "next-themes";
import { SunIcon, MoonIcon } from "@/components/icons";
import { Button } from "@/components/ui/button";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";

export const ThemeToggle = () => {
  const { setTheme } = useTheme();

  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="ghost" size="icon">
          <SunIcon className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
          <MoonIcon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
          <span className="sr-only">Toggle theme</span>
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end">
        <DropdownMenuItem onClick={() => setTheme("light")}>
          Light
        </DropdownMenuItem>
        <DropdownMenuItem onClick={() => setTheme("dark")}>
          Dark
        </DropdownMenuItem>
        <DropdownMenuItem onClick={() => setTheme("system")}>
          System
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  );
};


================================================
FILE: src/components/ui/alert-dialog.tsx
================================================
"use client"

import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"

import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"

const AlertDialog = AlertDialogPrimitive.Root

const AlertDialogTrigger = AlertDialogPrimitive.Trigger

const AlertDialogPortal = AlertDialogPrimitive.Portal

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

const AlertDialogContent = React.forwardRef<
  React.ElementRef<typeof AlertDialogPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
  <AlertDialogPortal>
    <AlertDialogOverlay />
    <AlertDialogPrimitive.Content
      ref={ref}
      className={cn(
        "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
        className
      )}
      {...props}
    />
  </AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName

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

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

const AlertDialogTitle = React.forwardRef<
  React.ElementRef<typeof AlertDialogPrimitive.Title>,
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
  <AlertDialogPrimitive.Title
    ref={ref}
    className={cn("text-lg font-semibold", className)}
    {...props}
  />
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName

const AlertDialogDescription = React.forwardRef<
  React.ElementRef<typeof AlertDialogPrimitive.Description>,
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
  <AlertDialogPrimitive.Description
    ref={ref}
    className={cn("text-sm text-muted-foreground", className)}
    {...props}
  />
))
AlertDialogDescription.displayName =
  AlertDialogPrimitive.Description.displayName

const AlertDialogAction = React.forwardRef<
  React.ElementRef<typeof AlertDialogPrimitive.Action>,
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
  <AlertDialogPrimitive.Action
    ref={ref}
    className={cn(buttonVariants(), className)}
    {...props}
  />
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName

const AlertDialogCancel = React.forwardRef<
  React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
  <AlertDialogPrimitive.Cancel
    ref={ref}
    className={cn(
      buttonVariants({ variant: "outline" }),
      "mt-2 sm:mt-0",
      className
    )}
    {...props}
  />
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName

export {
  AlertDialog,
  AlertDialogPortal,
  AlertDialogOverlay,
  AlertDialogTrigger,
  AlertDialogContent,
  AlertDialogHeader,
  AlertDialogFooter,
  AlertDialogTitle,
  AlertDialogDescription,
  AlertDialogAction,
  AlertDialogCancel,
}


================================================
FILE: src/components/ui/alert.tsx
================================================
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/lib/utils"

const alertVariants = cva(
  "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",
  {
    variants: {
      variant: {
        default: "bg-background text-foreground",
        destructive:
          "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
      },
    },
    defaultVariants: {
      variant: "default",
    },
  }
)

const Alert = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
  <div
    ref={ref}
    role="alert"
    className={cn(alertVariants({ variant }), className)}
    {...props}
  />
))
Alert.displayName = "Alert"

const AlertTitle = React.forwardRef<
  HTMLParagraphElement,
  React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
  <h5
    ref={ref}
    className={cn("mb-1 font-medium leading-none tracking-tight", className)}
    {...props}
  />
))
AlertTitle.displayName = "AlertTitle"

const AlertDescription = React.forwardRef<
  HTMLParagraphElement,
  React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn("text-sm [&_p]:leading-relaxed", className)}
    {...props}
  />
))
AlertDescription.displayName = "AlertDescription"

export { Alert, AlertTitle, AlertDescription }


================================================
FILE: src/components/ui/badge.tsx
================================================
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/lib/utils"

const badgeVariants = cva(
  "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",
  {
    variants: {
      variant: {
        default:
          "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
        secondary:
          "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
        destructive:
          "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
        outline: "text-foreground",
      },
    },
    defaultVariants: {
      variant: "default",
    },
  }
)

export interface BadgeProps
  extends React.HTMLAttributes<HTMLDivElement>,
    VariantProps<typeof badgeVariants> {}

function Badge({ className, variant, ...props }: BadgeProps) {
  return (
    <div className={cn(badgeVariants({ variant }), className)} {...props} />
  )
}

export { Badge, badgeVariants }


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

import { cn } from "@/lib/utils"

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

export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  asChild?: boolean
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, asChild = false, ...props }, ref) => {
    const Comp = asChild ? Slot : "button"
    return (
      <Comp
        className={cn(buttonVariants({ variant, size, className }))}
        ref={ref}
        {...props}
      />
    )
  }
)
Button.displayName = "Button"

export { Button, buttonVariants }


================================================
FILE: src/components/ui/card.tsx
================================================
import * as React from "react"

import { cn } from "@/lib/utils"

const Card = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn(
      "rounded-lg border bg-card text-card-foreground shadow-sm",
      className
    )}
    {...props}
  />
))
Card.displayName = "Card"

const CardHeader = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn("flex flex-col space-y-1.5 p-6", className)}
    {...props}
  />
))
CardHeader.displayName = "CardHeader"

const CardTitle = React.forwardRef<
  HTMLParagraphElement,
  React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
  <h3
    ref={ref}
    className={cn(
      "text-2xl font-semibold leading-none tracking-tight",
      className
    )}
    {...props}
  />
))
CardTitle.displayName = "CardTitle"

const CardDescription = React.forwardRef<
  HTMLParagraphElement,
  React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
  <p
    ref={ref}
    className={cn("text-sm text-muted-foreground", className)}
    {...props}
  />
))
CardDescription.displayName = "CardDescription"

const CardContent = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"

const CardFooter = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn("flex items-center p-6 pt-0", className)}
    {...props}
  />
))
CardFooter.displayName = "CardFooter"

export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }


================================================
FILE: src/components/ui/dialog.tsx
================================================
"use client"

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

import { cn } from "@/lib/utils"

const Dialog = DialogPrimitive.Root

const DialogTrigger = DialogPrimitive.Trigger

const DialogPortal = DialogPrimitive.Portal

const DialogClose = DialogPrimitive.Close

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

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

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

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

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

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

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


================================================
FILE: src/components/ui/drawer.tsx
================================================
"use client"

import * as React from "react"
import { Drawer as DrawerPrimitive } from "vaul"

import { cn } from "@/lib/utils"

const Drawer = ({
  shouldScaleBackground = true,
  ...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
  <DrawerPrimitive.Root
    shouldScaleBackground={shouldScaleBackground}
    {...props}
  />
)
Drawer.displayName = "Drawer"

const DrawerTrigger = DrawerPrimitive.Trigger

const DrawerPortal = DrawerPrimitive.Portal

const DrawerClose = DrawerPrimitive.Close

const DrawerOverlay = React.forwardRef<
  React.ElementRef<typeof DrawerPrimitive.Overlay>,
  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
>(({ className, ...props }, ref) => (
  <DrawerPrimitive.Overlay
    ref={ref}
    className={cn("fixed inset-0 z-50 bg-black/80", className)}
    {...props}
  />
))
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName

const DrawerContent = React.forwardRef<
  React.ElementRef<typeof DrawerPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
>(({ className, children, ...props }, ref) => (
  <DrawerPortal>
    <DrawerOverlay />
    <DrawerPrimitive.Content
      ref={ref}
      className={cn(
        "fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
        className
      )}
      {...props}
    >
      <div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
      {children}
    </DrawerPrimitive.Content>
  </DrawerPortal>
))
DrawerContent.displayName = "DrawerContent"

const DrawerHeader = ({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) => (
  <div
    className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
    {...props}
  />
)
DrawerHeader.displayName = "DrawerHeader"

const DrawerFooter = ({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) => (
  <div
    className={cn("mt-auto flex flex-col gap-2 p-4", className)}
    {...props}
  />
)
DrawerFooter.displayName = "DrawerFooter"

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

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

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


================================================
FILE: src/components/ui/dropdown-menu.tsx
================================================
"use client"

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

import { cn } from "@/lib/utils"

const DropdownMenu = DropdownMenuPrimitive.Root

const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger

const DropdownMenuGroup = DropdownMenuPrimitive.Group

const DropdownMenuPortal = DropdownMenuPrimitive.Portal

const DropdownMenuSub = DropdownMenuPrimitive.Sub

const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup

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

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

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

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

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

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

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

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

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

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


================================================
FILE: src/components/ui/form.tsx
================================================
import * as React from "react";
import type * as LabelPrimitive from "@radix-ui/react-label";
import { Slot } from "@radix-ui/react-slot";
import {
  Controller,
  type ControllerProps,
  type FieldPath,
  type FieldValues,
  FormProvider,
  useFormContext,
} from "react-hook-form";

import { cn } from "@/lib/utils";
import { Label } from "@/components/ui/label";

const Form = FormProvider;

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

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

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

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

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

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

  const { id } = itemContext;

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

type FormItemContextValue = {
  id: string;
};

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

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

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

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

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

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

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

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

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

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

  if (!body) {
    return null;
  }

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

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


================================================
FILE: src/components/ui/input.tsx
================================================
import * as React from "react"

import { cn } from "@/lib/utils"

export interface InputProps
  extends React.InputHTMLAttributes<HTMLInputElement> {}

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

export { Input }


================================================
FILE: src/components/ui/label.tsx
================================================
"use client"

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

import { cn } from "@/lib/utils"

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

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

export { Label }


================================================
FILE: src/components/ui/pagination.tsx
================================================
import * as React from "react";
import Link from "next/link";
import {
  ChevronLeftIcon,
  ChevronRightIcon,
  DotsHorizontalIcon,
} from "@/components/icons";

import { cn } from "@/lib/utils";
import { buttonVariants, type ButtonProps } from "@/components/ui/button";

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

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

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

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

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

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

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

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

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


================================================
FILE: src/components/ui/skeleton.tsx
================================================
import { cn } from "@/lib/utils"

function Skeleton({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) {
  return (
    <div
      className={cn("animate-pulse rounded-md bg-muted", className)}
      {...props}
    />
  )
}

export { Skeleton }


================================================
FILE: src/components/ui/sonner.tsx
================================================
"use client"

import { useTheme } from "next-themes"
import { Toaster as Sonner } from "sonner"

type ToasterProps = React.ComponentProps<typeof Sonner>

const Toaster = ({ ...props }: ToasterProps) => {
  const { theme = "system" } = useTheme()

  return (
    <Sonner
      theme={theme as ToasterProps["theme"]}
      className="toaster group"
      toastOptions={{
        classNames: {
          toast:
            "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
          description: "group-[.toast]:text-muted-foreground",
          actionButton:
            "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
          cancelButton:
            "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
        },
      }}
      {...props}
    />
  )
}

export { Toaster }


================================================
FILE: src/components/ui/tabs.tsx
================================================
"use client"

import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"

import { cn } from "@/lib/utils"

const Tabs = TabsPrimitive.Root

const TabsList = React.forwardRef<
  React.ElementRef<typeof TabsPrimitive.List>,
  React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
  <TabsPrimitive.List
    ref={ref}
    className={cn(
      "inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
      className
    )}
    {...props}
  />
))
TabsList.displayName = TabsPrimitive.List.displayName

const TabsTrigger = React.forwardRef<
  React.ElementRef<typeof TabsPrimitive.Trigger>,
  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
  <TabsPrimitive.Trigger
    ref={ref}
    className={cn(
      "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",
      className
    )}
    {...props}
  />
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName

const TabsContent = React.forwardRef<
  React.ElementRef<typeof TabsPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
  <TabsPrimitive.Content
    ref={ref}
    className={cn(
      "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
      className
    )}
    {...props}
  />
))
TabsContent.displayName = TabsPrimitive.Content.displayName

export { Tabs, TabsList, TabsTrigger, TabsContent }


================================================
FILE: src/components/ui/textarea.tsx
================================================
import * as React from "react"

import { cn } from "@/lib/utils"

export interface TextareaProps
  extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}

const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
  ({ className, ...props }, ref) => {
    return (
      <textarea
        className={cn(
          "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",
          className
        )}
        ref={ref}
        {...props}
      />
    )
  }
)
Textarea.displayName = "Textarea"

export { Textarea }


================================================
FILE: src/config/subscriptions.ts
================================================
import { env } from "@/env";

export interface SubscriptionPlan {
  name: string;
  description: string;
  features: string[];
  stripePriceId: string;
}

export const freePlan: SubscriptionPlan = {
  name: "Free",
  description: "The free plan is limited to 3 posts.",
  features: ["Up to 3 posts", "Limited support"],
  stripePriceId: "",
};

export const proPlan: SubscriptionPlan = {
  name: "Pro",
  description: "The Pro plan has unlimited posts.",
  features: ["Unlimited posts", "Priority support"],
  stripePriceId: env.STRIPE_PRO_MONTHLY_PLAN_ID,
};

export const subscriptionPlans = [freePlan, proPlan];


================================================
FILE: src/env.js
================================================
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";

export const env = createEnv({
  /**
   * Specify your server-side environment variables schema here. This way you can ensure the app
   * isn't built with invalid env vars.
   */
  server: {
    DATABASE_URL: z
      .string()
      .url()
      .refine(
        (str) => !str.includes("YOUR_DATABASE_URL_HERE"),
        "You forgot to change the default URL",
      ),
    NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
    MOCK_SEND_EMAIL: z.boolean().default(false),
    DISCORD_CLIENT_ID: z.string().trim().min(1),
    DISCORD_CLIENT_SECRET: z.string().trim().min(1),
    SMTP_HOST: z.string().trim().min(1),
    SMTP_PORT: z.number().int().min(1),
    SMTP_USER: z.string().trim().min(1),
    SMTP_PASSWORD: z.string().trim().min(1),
    STRIPE_API_KEY: z.string().trim().min(1),
    STRIPE_WEBHOOK_SECRET: z.string().trim().min(1),
    STRIPE_PRO_MONTHLY_PLAN_ID: z.string().trim().min(1),
  },

  /**
   * Specify your client-side environment variables schema here. This way you can ensure the app
   * isn't built with invalid env vars. To expose them to the client, prefix them with
   * `NEXT_PUBLIC_`.
   */
  client: {
    // NEXT_PUBLIC_CLIENTVAR: z.string(),
    NEXT_PUBLIC_APP_URL: z.string().url(),
  },

  /**
   * You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g.
   * middlewares) or client-side so we need to destruct manually.
   */
  runtimeEnv: {
    // Server-side env vars
    DATABASE_URL: process.env.DATABASE_URL,
    NODE_ENV: process.env.NODE_ENV,
    SMTP_HOST: process.env.SMTP_HOST,
    SMTP_PORT: parseInt(process.env.SMTP_PORT ?? ""),
    SMTP_USER: process.env.SMTP_USER,
    SMTP_PASSWORD: process.env.SMTP_PASSWORD,
    MOCK_SEND_EMAIL: process.env.MOCK_SEND_EMAIL === "true" || process.env.MOCK_SEND_EMAIL === "1",
    DISCORD_CLIENT_ID: process.env.DISCORD_CLIENT_ID,
    DISCORD_CLIENT_SECRET: process.env.DISCORD_CLIENT_SECRET,
    STRIPE_API_KEY: process.env.STRIPE_API_KEY,
    STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET,
    STRIPE_PRO_MONTHLY_PLAN_ID: process.env.STRIPE_PRO_MONTHLY_PLAN_ID,
    // Client-side env vars
    NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
  },
  /**
   * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
   * useful for Docker builds.
   */
  skipValidation: !!process.env.SKIP_ENV_VALIDATION,
  /**
   * Makes it so that empty strings are treated as undefined.
   * `SOME_VAR: z.string()` and `SOME_VAR=''` will throw an error.
   */
  emptyStringAsUndefined: true,
});


================================================
FILE: src/lib/auth/actions.ts
================================================
"use server";

/* eslint @typescript-eslint/no-explicit-any:0, @typescript-eslint/prefer-optional-chain:0 */

import { z } from "zod";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { generateId, Scrypt } from "lucia";
import { isWithinExpirationDate, TimeSpan, createDate } from "oslo";
import { generateRandomString, alphabet } from "oslo/crypto";
import { eq } from "drizzle-orm";
import { lucia } from "@/lib/auth";
import { db } from "@/server/db";
import {
  loginSchema,
  signupSchema,
  type LoginInput,
  type SignupInput,
  resetPasswordSchema,
} from "@/lib/validators/auth";
import { emailVerificationCodes, passwordResetTokens, users } from "@/server/db/schema";
import { sendMail, EmailTemplate } from "@/lib/email";
import { validateRequest } from "@/lib/auth/validate-request";
import { Paths } from "../constants";
import { env } from "@/env";

export interface ActionResponse<T> {
  fieldError?: Partial<Record<keyof T, string | undefined>>;
  formError?: string;
}

export async function login(_: any, formData: FormData): Promise<ActionResponse<LoginInput>> {
  const obj = Object.fromEntries(formData.entries());

  const parsed = loginSchema.safeParse(obj);
  if (!parsed.success) {
    const err = parsed.error.flatten();
    return {
      fieldError: {
        email: err.fieldErrors.email?.[0],
        password: err.fieldErrors.password?.[0],
      },
    };
  }

  const { email, password } = parsed.data;

  const existingUser = await db.query.users.findFirst({
    where: (table, { eq }) => eq(table.email, email),
  });

  if (!existingUser || !existingUser?.hashedPassword) {
    return {
      formError: "Incorrect email or password",
    };
  }

  const validPassword = await new Scrypt().verify(existingUser.hashedPassword, password);
  if (!validPassword) {
    return {
      formError: "Incorrect email or password",
    };
  }

  const session = await lucia.createSession(existingUser.id, {});
  const sessionCookie = lucia.createSessionCookie(session.id);
  cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
  return redirect(Paths.Dashboard);
}

export async function signup(_: any, formData: FormData): Promise<ActionResponse<SignupInput>> {
  const obj = Object.fromEntries(formData.entries());

  const parsed = signupSchema.safeParse(obj);
  if (!parsed.success) {
    const err = parsed.error.flatten();
    return {
      fieldError: {
        email: err.fieldErrors.email?.[0],
        password: err.fieldErrors.password?.[0],
      },
    };
  }

  const { email, password } = parsed.data;

  const existingUser = await db.query.users.findFirst({
    where: (table, { eq }) => eq(table.email, email),
    columns: { email: true },
  });

  if (existingUser) {
    return {
      formError: "Cannot create account with that email",
    };
  }

  const userId = generateId(21);
  const hashedPassword = await new Scrypt().hash(password);
  await db.insert(users).values({
    id: userId,
    email,
    hashedPassword,
  });

  const verificationCode = await generateEmailVerificationCode(userId, email);
  await sendMail(email, EmailTemplate.EmailVerification, { code: verificationCode });

  const session = await lucia.createSession(userId, {});
  const sessionCookie = lucia.createSessionCookie(session.id);
  cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
  return redirect(Paths.VerifyEmail);
}

export async function logout(): Promise<{ error: string } | void> {
  const { session } = await validateRequest();
  if (!session) {
    return {
      error: "No session found",
    };
  }
  await lucia.invalidateSession(session.id);
  const sessionCookie = lucia.createBlankSessionCookie();
  cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
  return redirect("/");
}

export async function resendVerificationEmail(): Promise<{
  error?: string;
  success?: boolean;
}> {
  const { user } = await validateRequest();
  if (!user) {
    return redirect(Paths.Login);
  }
  const lastSent = await db.query.emailVerificationCodes.findFirst({
    where: (table, { eq }) => eq(table.userId, user.id),
    columns: { expiresAt: true },
  });

  if (lastSent && isWithinExpirationDate(lastSent.expiresAt)) {
    return {
      error: `Please wait ${timeFromNow(lastSent.expiresAt)} before resending`,
    };
  }
  const verificationCode = await generateEmailVerificationCode(user.id, user.email);
  await sendMail(user.email, EmailTemplate.EmailVerification, { code: verificationCode });

  return { success: true };
}

export async function verifyEmail(_: any, formData: FormData): Promise<{ error: string } | void> {
  const code = formData.get("code");
  if (typeof code !== "string" || code.length !== 8) {
    return { error: "Invalid code" };
  }
  const { user } = await validateRequest();
  if (!user) {
    return redirect(Paths.Login);
  }

  const dbCode = await db.transaction(async (tx) => {
    const item = await tx.query.emailVerificationCodes.findFirst({
      where: (table, { eq }) => eq(table.userId, user.id),
    });
    if (item) {
      await tx.delete(emailVerificationCodes).where(eq(emailVerificationCodes.id, item.id));
    }
    return item;
  });

  if (!dbCode || dbCode.code !== code) return { error: "Invalid verification code" };

  if (!isWithinExpirationDate(dbCode.expiresAt)) return { error: "Verification code expired" };

  if (dbCode.email !== user.email) return { error: "Email does not match" };

  await lucia.invalidateUserSessions(user.id);
  await db.update(users).set({ emailVerified: true }).where(eq(users.id, user.id));
  const session = await lucia.createSession(user.id, {});
  const sessionCookie = lucia.createSessionCookie(session.id);
  cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
  redirect(Paths.Dashboard);
}

export async function sendPasswordResetLink(
  _: any,
  formData: FormData,
): Promise<{ error?: string; success?: boolean }> {
  const email = formData.get("email");
  const parsed = z.string().trim().email().safeParse(email);
  if (!parsed.success) {
    return { error: "Provided email is invalid." };
  }
  try {
    const user = await db.query.users.findFirst({
      where: (table, { eq }) => eq(table.email, parsed.data),
    });

    if (!user || !user.emailVerified) return { error: "Provided email is invalid." };

    const verificationToken = await generatePasswordResetToken(user.id);

    const verificationLink = `${env.NEXT_PUBLIC_APP_URL}/reset-password/${verificationToken}`;

    await sendMail(user.email, EmailTemplate.PasswordReset, { link: verificationLink });

    return { success: true };
  } catch (error) {
    return { error: "Failed to send verification email." };
  }
}

export async function resetPassword(
  _: any,
  formData: FormData,
): Promise<{ error?: string; success?: boolean }> {
  const obj = Object.fromEntries(formData.entries());

  const parsed = resetPasswordSchema.safeParse(obj);

  if (!parsed.success) {
    const err = parsed.error.flatten();
    return {
      error: err.fieldErrors.password?.[0] ?? err.fieldErrors.token?.[0],
    };
  }
  const { token, password } = parsed.data;

  const dbToken = await db.transaction(async (tx) => {
    const item = await tx.query.passwordResetTokens.findFirst({
      where: (table, { eq }) => eq(table.id, token),
    });
    if (item) {
      await tx.delete(passwordResetTokens).where(eq(passwordResetTokens.id, item.id));
    }
    return item;
  });

  if (!dbToken) return { error: "Invalid password reset link" };

  if (!isWithinExpirationDate(dbToken.expiresAt)) return { error: "Password reset link expired." };

  await lucia.invalidateUserSessions(dbToken.userId);
  const hashedPassword = await new Scrypt().hash(password);
  await db.update(users).set({ hashedPassword }).where(eq(users.id, dbToken.userId));
  const session = await lucia.createSession(dbToken.userId, {});
  const sessionCookie = lucia.createSessionCookie(session.id);
  cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
  redirect(Paths.Dashboard);
}

const timeFromNow = (time: Date) => {
  const now = new Date();
  const diff = time.getTime() - now.getTime();
  const minutes = Math.floor(diff / 1000 / 60);
  const seconds = Math.floor(diff / 1000) % 60;
  return `${minutes}m ${seconds}s`;
};

async function generateEmailVerificationCode(userId: string, email: string): Promise<string> {
  await db.delete(emailVerificationCodes).where(eq(emailVerificationCodes.userId, userId));
  const code = generateRandomString(8, alphabet("0-9")); // 8 digit code
  await db.insert(emailVerificationCodes).values({
    userId,
    email,
    code,
    expiresAt: createDate(new TimeSpan(10, "m")), // 10 minutes
  });
  return code;
}

async function generatePasswordResetToken(userId: string): Promise<string> {
  await db.delete(passwordResetTokens).where(eq(passwordResetTokens.userId, userId));
  const tokenId = generateId(40);
  await db.insert(passwordResetTokens).values({
    id: tokenId,
    userId,
    expiresAt: createDate(new TimeSpan(2, "h")),
  });
  return tokenId;
}


================================================
FILE: src/lib/auth/index.ts
================================================
import { Lucia, TimeSpan } from "lucia";
import { Discord } from "arctic";
import { DrizzlePostgreSQLAdapter } from "@lucia-auth/adapter-drizzle";
import { env } from "@/env.js";
import { db } from "@/server/db";
import { sessions, users, type User as DbUser } from "@/server/db/schema";
import { absoluteUrl } from "@/lib/utils"

// 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.
// import { webcrypto } from "node:crypto";
// globalThis.crypto = webcrypto as Crypto;

const adapter = new DrizzlePostgreSQLAdapter(db, sessions, users);

export const lucia = new Lucia(adapter, {
  getSessionAttributes: (/* attributes */) => {
    return {};
  },
  getUserAttributes: (attributes) => {
    return {
      id: attributes.id,
      email: attributes.email,
      emailVerified: attributes.emailVerified,
      avatar: attributes.avatar,
      createdAt: attributes.createdAt,
      updatedAt: attributes.updatedAt,
    };
  },
  sessionExpiresIn: new TimeSpan(30, "d"),
  sessionCookie: {
    name: "session",

    expires: false, // session cookies have very long lifespan (2 years)
    attributes: {
      secure: env.NODE_ENV === "production",
    },
  },
});

export const discord = new Discord(
  env.DISCORD_CLIENT_ID,
  env.DISCORD_CLIENT_SECRET,
  absoluteUrl("/login/discord/callback")
);

declare module "lucia" {
  interface Register {
    Lucia: typeof lucia;
    DatabaseSessionAttributes: DatabaseSessionAttributes;
    DatabaseUserAttributes: DatabaseUserAttributes;
  }
}

interface DatabaseSessionAttributes {}
interface DatabaseUserAttributes extends Omit<DbUser, "hashedPassword"> {}


================================================
FILE: src/lib/auth/validate-request.ts
================================================
import { cache } from "react";
import { cookies } from "next/headers";
import type { Session, User } from "lucia";
import { lucia } from "@/lib/auth";


export const uncachedValidateRequest = async (): Promise<
  { user: User; session: Session } | { user: null; session: null }
> => {
  const sessionId = cookies().get(lucia.sessionCookieName)?.value ?? null;
  if (!sessionId) {
    return { user: null, session: null };
  }
  const result = await lucia.validateSession(sessionId);
  // next.js throws when you attempt to set cookie when rendering page
  try {
    if (result.session && result.session.fresh) {
      const sessionCookie = lucia.createSessionCookie(result.session.id);
      cookies().set(
        sessionCookie.name,
        sessionCookie.value,
        sessionCookie.attributes,
      );
    }
    if (!result.session) {
      const sessionCookie = lucia.createBlankSessionCookie();
      cookies().set(
        sessionCookie.name,
        sessionCookie.value,
        sessionCookie.attributes,
      );
    }
  } catch {
    console.error("Failed to set session cookie");
  }
  return result;
};

export const validateRequest = cache(uncachedValidateRequest);


================================================
FILE: src/lib/constants.ts
================================================
export const APP_TITLE = "Acme";
export const DATABASE_PREFIX = "acme";
export const TEST_DB_PREFIX = "test_acme";
export const EMAIL_SENDER = '"Acme" <noreply@acme.com>';

export enum Paths {
  Home = "/",
  Login = "/login",
  Signup = "/signup",
  Dashboard = "/dashboard",
  VerifyEmail = "/verify-email",
  ResetPassword = "/reset-password",
}


================================================
FILE: src/lib/email/index.tsx
================================================
import "server-only";

import { EmailVerificationTemplate } from "./templates/email-verification";
import { ResetPasswordTemplate } from "./templates/reset-password";
import { render } from "@react-email/render";
import { env } from "@/env";
import { EMAIL_SENDER } from "@/lib/constants";
import { createTransport, type TransportOptions } from "nodemailer";
import type { ComponentProps } from "react";
import { logger } from "../logger";

export enum EmailTemplate {
  EmailVerification = "EmailVerification",
  PasswordReset = "PasswordReset",
}

export type PropsMap = {
  [EmailTemplate.EmailVerification]: ComponentProps<typeof EmailVerificationTemplate>;
  [EmailTemplate.PasswordReset]: ComponentProps<typeof ResetPasswordTemplate>;
};

const getEmailTemplate = <T extends EmailTemplate>(template: T, props: PropsMap[NoInfer<T>]) => {
  switch (template) {
    case EmailTemplate.EmailVerification:
      return {
        subject: "Verify your email address",
        body: render(
          <EmailVerificationTemplate {...(props as PropsMap[EmailTemplate.EmailVerification])} />,
        ),
      };
    case EmailTemplate.PasswordReset:
      return {
        subject: "Reset your password",
        body: render(
          <ResetPasswordTemplate {...(props as PropsMap[EmailTemplate.PasswordReset])} />,
        ),
      };
    default:
      throw new Error("Invalid email template");
  }
};

const smtpConfig = {
  host: env.SMTP_HOST,
  port: env.SMTP_PORT,
  auth: {
    user: env.SMTP_USER,
    pass: env.SMTP_PASSWORD,
  },
};

const transporter = createTransport(smtpConfig as TransportOptions);

export const sendMail = async <T extends EmailTemplate>(
  to: string,
  template: T,
  props: PropsMap[NoInfer<T>],
) => {
  if (env.MOCK_SEND_EMAIL) {
    logger.info("📨 Email sent to:", to, "with template:", template, "and props:", props);
    return;
  }

  const { subject, body } = getEmailTemplate(template, props);

  return transporter.sendMail({ from: EMAIL_SENDER, to, subject, html: body });
};


================================================
FILE: src/lib/email/templates/email-verification.tsx
================================================
import { Body, Container, Head, Html, Preview, Section, Text } from "@react-email/components";
import { APP_TITLE } from "@/lib/constants";

export interface EmailVerificationTemplateProps {
  code: string;
}

export const EmailVerificationTemplate = ({ code }: EmailVerificationTemplateProps) => {
  return (
    <Html>
      <Head />
      <Preview>Verify your email address to complete your {APP_TITLE} registration</Preview>
      <Body style={main}>
        <Container style={container}>
          <Section>
            <Text style={title}>{APP_TITLE}</Text>
            <Text style={text}>Hi,</Text>
            <Text style={text}>
              Thank you for registering for an account on {APP_TITLE}. To complete your
              registration, please verify your your account by using the following code:
            </Text>
            <Text style={codePlaceholder}>{code}</Text>

            <Text style={text}>Have a nice day!</Text>
          </Section>
        </Container>
      </Body>
    </Html>
  );
};

const main = {
  backgroundColor: "#f6f9fc",
  padding: "10px 0",
};

const container = {
  backgroundColor: "#ffffff",
  border: "1px solid #f0f0f0",
  padding: "45px",
};

const text = {
  fontSize: "16px",
  fontFamily:
    "'Open Sans', 'HelveticaNeue-Light', 'Helvetica Neue Light', 'Helvetica Neue', Helvetica, Arial, 'Lucida Grande', sans-serif",
  fontWeight: "300",
  color: "#404040",
  lineHeight: "26px",
};

const title = {
  ...text,
  fontSize: "22px",
  fontWeight: "700",
  lineHeight: "32px",
};

const codePlaceholder = {
  backgroundColor: "#fbfbfb",
  border: "1px solid #f0f0f0",
  borderRadius: "4px",
  color: "#1c1c1c",
  fontFamily: "'Open Sans', 'Helvetica Neue', Arial",
  fontSize: "15px",
  textDecoration: "none",
  textAlign: "center" as const,
  display: "block",
  width: "210px",
  padding: "14px 7px",
};

// const anchor = {
//   textDecoration: "underline",
// };


================================================
FILE: src/lib/email/templates/reset-password.tsx
================================================
import { render } from "@react-email/render";
import {
  Body,
  Button,
  Container,
  Head,
  Html,
  Preview,
  Section,
  Text,
} from "@react-email/components";
import { APP_TITLE } from "@/lib/constants";

export interface ResetPasswordTemplateProps {
  link: string;
}

export const ResetPasswordTemplate = ({ link }: ResetPasswordTemplateProps) => {
  return (
    <Html>
      <Head />
      <Preview>Reset your password</Preview>
      <Body style={main}>
        <Container style={container}>
          <Section>
            <Text style={title}>{APP_TITLE}</Text>
            <Text style={text}>Hi,</Text>
            <Text style={text}>
              Someone recently requested a password change for your {APP_TITLE} account. If this was
              you, you can set a new password here:
            </Text>
            <Button style={button} href={link}>
              Reset password
            </Button>
            <Text style={text}>
              If you don&apos;t want to change your password or didn&apos;t request this, just
              ignore and delete this message.
            </Text>
            <Text style={text}>
              To keep your account secure, please don&apos;t forward this email to anyone.
            </Text>
            <Text style={text}>Have a nice day!</Text>
          </Section>
        </Container>
      </Body>
    </Html>
  );
};

const main = {
  backgroundColor: "#f6f9fc",
  padding: "10px 0",
};

const container = {
  backgroundColor: "#ffffff",
  border: "1px solid #f0f0f0",
  padding: "45px",
};

const text = {
  fontSize: "16px",
  fontFamily:
    "'Open Sans', 'HelveticaNeue-Light', 'Helvetica Neue Light', 'Helvetica Neue', Helvetica, Arial, 'Lucida Grande', sans-serif",
  fontWeight: "300",
  color: "#404040",
  lineHeight: "26px",
};

const title = {
  ...text,
  fontSize: "22px",
  fontWeight: "700",
  lineHeight: "32px",
};

const button = {
  backgroundColor: "#09090b",
  borderRadius: "4px",
  color: "#fafafa",
  fontFamily: "'Open Sans', 'Helvetica Neue', Arial",
  fontSize: "15px",
  textDecoration: "none",
  textAlign: "center" as const,
  display: "block",
  width: "210px",
  padding: "14px 7px",
};

// const anchor = {
//   textDecoration: "underline",
// };


================================================
FILE: src/lib/fonts.ts
================================================
import "@/styles/globals.css";

import { Inter as FontSans } from "next/font/google";

export const fontSans = FontSans({
  subsets: ["latin"],
  variable: "--font-sans",
});


================================================
FILE: src/lib/hooks/use-debounce.ts
================================================
import { useEffect, useState } from "react";

export function useDebounce<T>(value: T, delay: number) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);

  return debouncedValue;
}


================================================
FILE: src/lib/hooks/use-media-query.ts
================================================
import * as React from "react";

export function useMediaQuery(query: string) {
  const [value, setValue] = React.useState(false);

  React.useEffect(() => {
    function onChange(event: MediaQueryListEvent) {
      setValue(event.matches);
    }

    const result = matchMedia(query);
    result.addEventListener("change", onChange);
    setValue(result.matches);

    return () => result.removeEventListener("change", onChange);
  }, [query]);

  return value;
}


================================================
FILE: src/lib/logger.ts
=========
Download .txt
gitextract_w3daecuw/

├── .devcontainer/
│   ├── Dockerfile
│   ├── compose.dev.yml
│   └── devcontainer.json
├── .eslintrc.cjs
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   ├── pull_request_template.md
│   └── workflows/
│       └── check.yaml
├── .gitignore
├── LICENSE
├── README.md
├── components.json
├── drizzle.config.ts
├── next.config.js
├── package.json
├── playwright.config.ts
├── postcss.config.cjs
├── prettier.config.js
├── src/
│   ├── app/
│   │   ├── (auth)/
│   │   │   ├── layout.tsx
│   │   │   ├── login/
│   │   │   │   ├── discord/
│   │   │   │   │   ├── callback/
│   │   │   │   │   │   └── route.ts
│   │   │   │   │   └── route.ts
│   │   │   │   ├── login.tsx
│   │   │   │   └── page.tsx
│   │   │   ├── reset-password/
│   │   │   │   ├── [token]/
│   │   │   │   │   ├── page.tsx
│   │   │   │   │   └── reset-password.tsx
│   │   │   │   ├── page.tsx
│   │   │   │   └── send-reset-email.tsx
│   │   │   ├── signup/
│   │   │   │   ├── page.tsx
│   │   │   │   └── signup.tsx
│   │   │   └── verify-email/
│   │   │       ├── page.tsx
│   │   │       └── verify-code.tsx
│   │   ├── (landing)/
│   │   │   ├── _components/
│   │   │   │   ├── copy-to-clipboard.tsx
│   │   │   │   ├── feature-icons.tsx
│   │   │   │   ├── footer.tsx
│   │   │   │   ├── header.tsx
│   │   │   │   └── hover-card.tsx
│   │   │   ├── layout.tsx
│   │   │   └── page.tsx
│   │   ├── (main)/
│   │   │   ├── _components/
│   │   │   │   ├── footer.tsx
│   │   │   │   ├── header.tsx
│   │   │   │   └── user-dropdown.tsx
│   │   │   ├── account/
│   │   │   │   └── page.tsx
│   │   │   ├── dashboard/
│   │   │   │   ├── _components/
│   │   │   │   │   ├── dashboard-nav.tsx
│   │   │   │   │   ├── new-post.tsx
│   │   │   │   │   ├── post-card-skeleton.tsx
│   │   │   │   │   ├── post-card.tsx
│   │   │   │   │   ├── posts-skeleton.tsx
│   │   │   │   │   ├── posts.tsx
│   │   │   │   │   └── verificiation-warning.tsx
│   │   │   │   ├── billing/
│   │   │   │   │   ├── _components/
│   │   │   │   │   │   ├── billing-skeleton.tsx
│   │   │   │   │   │   ├── billing.tsx
│   │   │   │   │   │   └── manage-subscription-form.tsx
│   │   │   │   │   └── page.tsx
│   │   │   │   ├── layout.tsx
│   │   │   │   ├── page.tsx
│   │   │   │   └── settings/
│   │   │   │       └── page.tsx
│   │   │   ├── editor/
│   │   │   │   └── [postId]/
│   │   │   │       ├── _components/
│   │   │   │       │   ├── post-editor.tsx
│   │   │   │       │   └── post-preview.tsx
│   │   │   │       └── page.tsx
│   │   │   └── layout.tsx
│   │   ├── api/
│   │   │   ├── trpc/
│   │   │   │   └── [trpc]/
│   │   │   │       └── route.ts
│   │   │   └── webhooks/
│   │   │       └── stripe/
│   │   │           └── route.ts
│   │   ├── icon.tsx
│   │   ├── layout.tsx
│   │   ├── robots.ts
│   │   └── sitemap.ts
│   ├── components/
│   │   ├── icons.tsx
│   │   ├── loading-button.tsx
│   │   ├── password-input.tsx
│   │   ├── responsive-dialog.tsx
│   │   ├── submit-button.tsx
│   │   ├── theme-provider.tsx
│   │   ├── theme-toggle.tsx
│   │   └── ui/
│   │       ├── alert-dialog.tsx
│   │       ├── alert.tsx
│   │       ├── badge.tsx
│   │       ├── button.tsx
│   │       ├── card.tsx
│   │       ├── dialog.tsx
│   │       ├── drawer.tsx
│   │       ├── dropdown-menu.tsx
│   │       ├── form.tsx
│   │       ├── input.tsx
│   │       ├── label.tsx
│   │       ├── pagination.tsx
│   │       ├── skeleton.tsx
│   │       ├── sonner.tsx
│   │       ├── tabs.tsx
│   │       └── textarea.tsx
│   ├── config/
│   │   └── subscriptions.ts
│   ├── env.js
│   ├── lib/
│   │   ├── auth/
│   │   │   ├── actions.ts
│   │   │   ├── index.ts
│   │   │   └── validate-request.ts
│   │   ├── constants.ts
│   │   ├── email/
│   │   │   ├── index.tsx
│   │   │   └── templates/
│   │   │       ├── email-verification.tsx
│   │   │       └── reset-password.tsx
│   │   ├── fonts.ts
│   │   ├── hooks/
│   │   │   ├── use-debounce.ts
│   │   │   └── use-media-query.ts
│   │   ├── logger.ts
│   │   ├── stripe.ts
│   │   ├── utils.ts
│   │   └── validators/
│   │       └── auth.ts
│   ├── middleware.ts
│   ├── server/
│   │   ├── api/
│   │   │   ├── root.ts
│   │   │   ├── routers/
│   │   │   │   ├── post/
│   │   │   │   │   ├── post.input.ts
│   │   │   │   │   ├── post.procedure.ts
│   │   │   │   │   └── post.service.ts
│   │   │   │   ├── stripe/
│   │   │   │   │   ├── stripe.input.ts
│   │   │   │   │   ├── stripe.procedure.ts
│   │   │   │   │   └── stripe.service.ts
│   │   │   │   └── user/
│   │   │   │       └── user.procedure.ts
│   │   │   └── trpc.ts
│   │   └── db/
│   │       ├── index.ts
│   │       └── schema.ts
│   ├── styles/
│   │   └── globals.css
│   └── trpc/
│       ├── react.tsx
│       ├── server.ts
│       └── shared.ts
├── tailwind.config.ts
├── tests/
│   └── e2e/
│       ├── auth-with-credential.spec.ts
│       └── utils.ts
└── tsconfig.json
Download .txt
SYMBOL INDEX (123 symbols across 69 files)

FILE: src/app/(auth)/login/discord/callback/route.ts
  function GET (line 10) | async function GET(request: Request): Promise<Response> {
  type DiscordUser (line 101) | interface DiscordUser {

FILE: src/app/(auth)/login/discord/route.ts
  function GET (line 6) | async function GET(): Promise<Response> {

FILE: src/app/(auth)/login/login.tsx
  function Login (line 15) | function Login() {

FILE: src/app/(auth)/login/page.tsx
  function LoginPage (line 11) | async function LoginPage() {

FILE: src/app/(auth)/reset-password/[token]/page.tsx
  function ResetPasswordPage (line 15) | function ResetPasswordPage({

FILE: src/app/(auth)/reset-password/[token]/reset-password.tsx
  function ResetPassword (line 12) | function ResetPassword({ token }: { token: string }) {

FILE: src/app/(auth)/reset-password/page.tsx
  function ForgotPasswordPage (line 18) | async function ForgotPasswordPage() {

FILE: src/app/(auth)/reset-password/send-reset-email.tsx
  function SendResetEmail (line 16) | function SendResetEmail() {

FILE: src/app/(auth)/signup/page.tsx
  function SignupPage (line 11) | async function SignupPage() {

FILE: src/app/(auth)/signup/signup.tsx
  function Signup (line 15) | function Signup() {

FILE: src/app/(auth)/verify-email/page.tsx
  function VerifyEmailPage (line 18) | async function VerifyEmailPage() {

FILE: src/app/(landing)/_components/feature-icons.tsx
  constant TRPC (line 210) | const TRPC = forwardRef<SVGSVGElement, SVGProps<SVGSVGElement>>(({ class...

FILE: src/app/(landing)/_components/hover-card.tsx
  type FeaturesProps (line 5) | type FeaturesProps = {

FILE: src/app/(landing)/layout.tsx
  function LandingPageLayout (line 12) | function LandingPageLayout({ children }: { children: ReactNode }) {

FILE: src/app/(landing)/page.tsx
  function NextjsIcon (line 139) | function NextjsIcon({ className }: { className?: string }) {

FILE: src/app/(main)/account/page.tsx
  function AccountPage (line 15) | async function AccountPage() {

FILE: src/app/(main)/dashboard/_components/dashboard-nav.tsx
  type Props (line 28) | interface Props {
  function DashboardNav (line 32) | function DashboardNav({ className }: Props) {

FILE: src/app/(main)/dashboard/_components/new-post.tsx
  type NewPostProps (line 10) | interface NewPostProps {

FILE: src/app/(main)/dashboard/_components/post-card-skeleton.tsx
  function PostCardSkeleton (line 4) | function PostCardSkeleton() {

FILE: src/app/(main)/dashboard/_components/post-card.tsx
  type PostCardProps (line 21) | interface PostCardProps {

FILE: src/app/(main)/dashboard/_components/posts-skeleton.tsx
  function PostsSkeleton (line 3) | function PostsSkeleton() {

FILE: src/app/(main)/dashboard/_components/posts.tsx
  type PostsProps (line 8) | interface PostsProps {
  function Posts (line 12) | function Posts({ promises }: PostsProps) {

FILE: src/app/(main)/dashboard/_components/verificiation-warning.tsx
  function VerificiationWarning (line 8) | async function VerificiationWarning() {

FILE: src/app/(main)/dashboard/billing/_components/billing-skeleton.tsx
  function BillingSkeleton (line 4) | function BillingSkeleton() {

FILE: src/app/(main)/dashboard/billing/_components/billing.tsx
  type BillingProps (line 18) | interface BillingProps {
  function Billing (line 24) | async function Billing({ stripePromises }: BillingProps) {

FILE: src/app/(main)/dashboard/billing/_components/manage-subscription-form.tsx
  function ManageSubscriptionForm (line 10) | function ManageSubscriptionForm({

FILE: src/app/(main)/dashboard/billing/page.tsx
  function BillingPage (line 21) | async function BillingPage() {

FILE: src/app/(main)/dashboard/layout.tsx
  type Props (line 4) | interface Props {
  function DashboardLayout (line 8) | function DashboardLayout({ children }: Props) {

FILE: src/app/(main)/dashboard/page.tsx
  type Props (line 18) | interface Props {
  function DashboardPage (line 22) | async function DashboardPage({ searchParams }: Props) {

FILE: src/app/(main)/dashboard/settings/page.tsx
  function BillingPage (line 13) | async function BillingPage() {

FILE: src/app/(main)/editor/[postId]/_components/post-editor.tsx
  type Props (line 27) | interface Props {

FILE: src/app/(main)/editor/[postId]/page.tsx
  type Props (line 10) | interface Props {
  function EditPostPage (line 16) | async function EditPostPage({ params }: Props) {

FILE: src/app/api/webhooks/stripe/route.ts
  function POST (line 11) | async function POST(req: Request) {

FILE: src/app/icon.tsx
  function Icon (line 14) | function Icon() {

FILE: src/app/layout.tsx
  function RootLayout (line 27) | function RootLayout({

FILE: src/app/robots.ts
  function robots (line 5) | function robots(): MetadataRoute.Robots {

FILE: src/app/sitemap.ts
  function sitemap (line 5) | async function sitemap(): Promise<MetadataRoute.Sitemap> {

FILE: src/components/loading-button.tsx
  type LoadingButtonProps (line 9) | interface LoadingButtonProps extends ButtonProps {

FILE: src/components/responsive-dialog.tsx
  type StatefulContent (line 32) | type StatefulContent = ({

FILE: src/components/theme-provider.tsx
  function ThemeProvider (line 7) | function ThemeProvider({ children, ...props }: ThemeProviderProps) {

FILE: src/components/ui/badge.tsx
  type BadgeProps (line 26) | interface BadgeProps
  function Badge (line 30) | function Badge({ className, variant, ...props }: BadgeProps) {

FILE: src/components/ui/button.tsx
  type ButtonProps (line 37) | interface ButtonProps

FILE: src/components/ui/form.tsx
  type FormFieldContextValue (line 18) | type FormFieldContextValue<
  type FormItemContextValue (line 65) | type FormItemContextValue = {

FILE: src/components/ui/input.tsx
  type InputProps (line 5) | interface InputProps

FILE: src/components/ui/pagination.tsx
  type PaginationLinkProps (line 42) | type PaginationLinkProps = {

FILE: src/components/ui/skeleton.tsx
  function Skeleton (line 3) | function Skeleton({

FILE: src/components/ui/sonner.tsx
  type ToasterProps (line 6) | type ToasterProps = React.ComponentProps<typeof Sonner>

FILE: src/components/ui/textarea.tsx
  type TextareaProps (line 5) | interface TextareaProps

FILE: src/config/subscriptions.ts
  type SubscriptionPlan (line 3) | interface SubscriptionPlan {

FILE: src/lib/auth/actions.ts
  type ActionResponse (line 27) | interface ActionResponse<T> {
  function login (line 32) | async function login(_: any, formData: FormData): Promise<ActionResponse...
  function signup (line 71) | async function signup(_: any, formData: FormData): Promise<ActionRespons...
  function logout (line 115) | async function logout(): Promise<{ error: string } | void> {
  function resendVerificationEmail (line 128) | async function resendVerificationEmail(): Promise<{
  function verifyEmail (line 152) | async function verifyEmail(_: any, formData: FormData): Promise<{ error:...
  function sendPasswordResetLink (line 186) | async function sendPasswordResetLink(
  function resetPassword (line 214) | async function resetPassword(
  function generateEmailVerificationCode (line 261) | async function generateEmailVerificationCode(userId: string, email: stri...
  function generatePasswordResetToken (line 273) | async function generatePasswordResetToken(userId: string): Promise<strin...

FILE: src/lib/auth/index.ts
  type Register (line 47) | interface Register {
  type DatabaseSessionAttributes (line 54) | interface DatabaseSessionAttributes {}
  type DatabaseUserAttributes (line 55) | interface DatabaseUserAttributes extends Omit<DbUser, "hashedPassword"> {}

FILE: src/lib/constants.ts
  constant APP_TITLE (line 1) | const APP_TITLE = "Acme";
  constant DATABASE_PREFIX (line 2) | const DATABASE_PREFIX = "acme";
  constant TEST_DB_PREFIX (line 3) | const TEST_DB_PREFIX = "test_acme";
  constant EMAIL_SENDER (line 4) | const EMAIL_SENDER = '"Acme" <noreply@acme.com>';
  type Paths (line 6) | enum Paths {

FILE: src/lib/email/index.tsx
  type EmailTemplate (line 12) | enum EmailTemplate {
  type PropsMap (line 17) | type PropsMap = {

FILE: src/lib/email/templates/email-verification.tsx
  type EmailVerificationTemplateProps (line 4) | interface EmailVerificationTemplateProps {

FILE: src/lib/email/templates/reset-password.tsx
  type ResetPasswordTemplateProps (line 14) | interface ResetPasswordTemplateProps {

FILE: src/lib/hooks/use-debounce.ts
  function useDebounce (line 3) | function useDebounce<T>(value: T, delay: number) {

FILE: src/lib/hooks/use-media-query.ts
  function useMediaQuery (line 3) | function useMediaQuery(query: string) {

FILE: src/lib/logger.ts
  type LogLevel (line 5) | enum LogLevel {
  class Logger (line 12) | class Logger {
    method constructor (line 16) | constructor(level: LogLevel = LogLevel.INFO, logFilePath = "applicatio...
    method getTimestamp (line 21) | private getTimestamp(): string {
    method formatMessage (line 25) | private formatMessage(level: LogLevel, args: unknown[]): string {
    method log (line 37) | private log(level: LogLevel, ...args: unknown[]): void {
    method shouldLog (line 46) | private shouldLog(level: LogLevel): boolean {
    method debug (line 51) | debug(...args: unknown[]): void {
    method info (line 55) | info(...args: unknown[]): void {
    method warn (line 59) | warn(...args: unknown[]): void {
    method error (line 63) | error(...args: unknown[]): void {

FILE: src/lib/utils.ts
  function cn (line 5) | function cn(...inputs: ClassValue[]) {
  function formatDate (line 29) | function formatDate(
  function formatPrice (line 42) | function formatPrice(
  function absoluteUrl (line 54) | function absoluteUrl(path: string) {

FILE: src/lib/validators/auth.ts
  type SignupInput (line 7) | type SignupInput = z.infer<typeof signupSchema>;
  type LoginInput (line 16) | type LoginInput = z.infer<typeof loginSchema>;
  type ForgotPasswordInput (line 21) | type ForgotPasswordInput = z.infer<typeof forgotPasswordSchema>;
  type ResetPasswordInput (line 27) | type ResetPasswordInput = z.infer<typeof resetPasswordSchema>;

FILE: src/middleware.ts
  function middleware (line 6) | async function middleware(request: NextRequest): Promise<NextResponse> {

FILE: src/server/api/root.ts
  type AppRouter (line 12) | type AppRouter = typeof appRouter;

FILE: src/server/api/routers/post/post.input.ts
  type ListPostsInput (line 7) | type ListPostsInput = z.infer<typeof listPostsSchema>;
  type GetPostInput (line 12) | type GetPostInput = z.infer<typeof getPostSchema>;
  type CreatePostInput (line 19) | type CreatePostInput = z.infer<typeof createPostSchema>;
  type UpdatePostInput (line 24) | type UpdatePostInput = z.infer<typeof updatePostSchema>;
  type DeletePostInput (line 29) | type DeletePostInput = z.infer<typeof deletePostSchema>;
  type MyPostsInput (line 35) | type MyPostsInput = z.infer<typeof myPostsSchema>;

FILE: src/server/api/routers/stripe/stripe.input.ts
  type ManageSubscriptionInput (line 10) | type ManageSubscriptionInput = z.infer<typeof manageSubscriptionSchema>;

FILE: src/server/api/trpc.ts
  method errorFormatter (line 49) | errorFormatter({ shape, error }) {
  type TRPCContext (line 104) | type TRPCContext = inferAsyncReturnType<typeof createTRPCContext>;
  type ProtectedTRPCContext (line 105) | type ProtectedTRPCContext = TRPCContext & {

FILE: src/server/db/schema.ts
  type User (line 37) | type User = typeof users.$inferSelect;
  type NewUser (line 38) | type NewUser = typeof users.$inferInsert;
  type Post (line 107) | type Post = typeof posts.$inferSelect;
  type NewPost (line 108) | type NewPost = typeof posts.$inferInsert;

FILE: src/trpc/react.tsx
  function TRPCReactProvider (line 13) | function TRPCReactProvider(props: { children: React.ReactNode }) {

FILE: src/trpc/shared.ts
  function getBaseUrl (line 8) | function getBaseUrl() {
  function getUrl (line 14) | function getUrl() {
  type RouterInputs (line 23) | type RouterInputs = inferRouterInputs<AppRouter>;
  type RouterOutputs (line 30) | type RouterOutputs = inferRouterOutputs<AppRouter>;

FILE: tests/e2e/utils.ts
  function extractLastCode (line 7) | function extractLastCode(log: string): string | null {
Condensed preview — 125 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (237K chars).
[
  {
    "path": ".devcontainer/Dockerfile",
    "chars": 57,
    "preview": "FROM mcr.microsoft.com/devcontainers/javascript-node:20 \n"
  },
  {
    "path": ".devcontainer/compose.dev.yml",
    "chars": 350,
    "preview": "services:\n  workspace:\n    build: \n      dockerfile: Dockerfile\n    volumes:\n      - ../:/workspace:cached\n    command: "
  },
  {
    "path": ".devcontainer/devcontainer.json",
    "chars": 971,
    "preview": "{\n  \"name\": \"Next.js Auth Template\",\n  \"dockerComposeFile\": [\"compose.dev.yml\"],\n  \"service\": \"workspace\",\n  \"workspaceF"
  },
  {
    "path": ".eslintrc.cjs",
    "chars": 1143,
    "preview": "/** @type {import(\"eslint\").Linter.Config} */\nconst config = {\n  parser: \"@typescript-eslint/parser\",\n  parserOptions: {"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "chars": 834,
    "preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the b"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "chars": 595,
    "preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Is your fea"
  },
  {
    "path": ".github/pull_request_template.md",
    "chars": 452,
    "preview": "# Description\n\nPlease include a summary of the changes and the related issue. Please also include relevant motivation an"
  },
  {
    "path": ".github/workflows/check.yaml",
    "chars": 1946,
    "preview": "name: Lint & Test\n\non:\n  pull_request:\n    branches: [main]\n\nconcurrency:\n  group: ci-${{ github.ref }}\n  cancel-in-prog"
  },
  {
    "path": ".gitignore",
    "chars": 692,
    "preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
  },
  {
    "path": "LICENSE",
    "chars": 1075,
    "preview": "# MIT License\n\nCopyright (c) [2023] [Touha Zohair]\n\nPermission is hereby granted, free of charge, to any person obtainin"
  },
  {
    "path": "README.md",
    "chars": 3855,
    "preview": "# Next.js Auth Starter Template\n\n## Motivation\n\nImplementing authentication in Next.js, especially Email+Password authen"
  },
  {
    "path": "components.json",
    "chars": 330,
    "preview": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"default\",\n  \"rsc\": true,\n  \"tsx\": true,\n  \"tailwind\": {\n"
  },
  {
    "path": "drizzle.config.ts",
    "chars": 316,
    "preview": "import { defineConfig } from \"drizzle-kit\";\nimport { DATABASE_PREFIX } from \"@/lib/constants\";\n\nexport default defineCon"
  },
  {
    "path": "next.config.js",
    "chars": 115,
    "preview": "await import(\"./src/env.js\");\n\n/** @type {import(\"next\").NextConfig} */\nconst config = {};\n\nexport default config;\n"
  },
  {
    "path": "package.json",
    "chars": 2796,
    "preview": "{\n  \"name\": \"next-lucia-auth\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": "
  },
  {
    "path": "playwright.config.ts",
    "chars": 755,
    "preview": "import { defineConfig, devices } from \"@playwright/test\";\nimport \"dotenv/config\";\n\nconst baseURL = `http://localhost:${p"
  },
  {
    "path": "postcss.config.cjs",
    "chars": 107,
    "preview": "const config = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n};\n\nmodule.exports = config;\n"
  },
  {
    "path": "prettier.config.js",
    "chars": 253,
    "preview": "/** @type {import('prettier').Config & import('prettier-plugin-tailwindcss').PluginOptions} */\nconst config = {\n  plugin"
  },
  {
    "path": "src/app/(auth)/layout.tsx",
    "chars": 231,
    "preview": "import type { ReactNode } from \"react\";\n\nconst AuthLayout = ({ children }: { children: ReactNode }) => {\n  return (\n    "
  },
  {
    "path": "src/app/(auth)/login/discord/callback/route.ts",
    "chars": 3516,
    "preview": "import { cookies } from \"next/headers\";\nimport { generateId } from \"lucia\";\nimport { OAuth2RequestError } from \"arctic\";"
  },
  {
    "path": "src/app/(auth)/login/discord/route.ts",
    "chars": 542,
    "preview": "import { cookies } from \"next/headers\";\nimport { generateState } from \"arctic\";\nimport { discord } from \"@/lib/auth\";\nim"
  },
  {
    "path": "src/app/(auth)/login/login.tsx",
    "chars": 3418,
    "preview": "\"use client\";\n\nimport Link from \"next/link\";\nimport { useFormState } from \"react-dom\";\nimport { Input } from \"@/componen"
  },
  {
    "path": "src/app/(auth)/login/page.tsx",
    "chars": 410,
    "preview": "import { redirect } from \"next/navigation\";\nimport { validateRequest } from \"@/lib/auth/validate-request\";\nimport { Path"
  },
  {
    "path": "src/app/(auth)/reset-password/[token]/page.tsx",
    "chars": 675,
    "preview": "import {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card\";\nimport { Re"
  },
  {
    "path": "src/app/(auth)/reset-password/[token]/reset-password.tsx",
    "chars": 1166,
    "preview": "\"use client\";\n\nimport { useEffect } from \"react\";\nimport { useFormState } from \"react-dom\";\nimport { toast } from \"sonne"
  },
  {
    "path": "src/app/(auth)/reset-password/page.tsx",
    "chars": 881,
    "preview": "import { redirect } from \"next/navigation\";\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle"
  },
  {
    "path": "src/app/(auth)/reset-password/send-reset-email.tsx",
    "chars": 1795,
    "preview": "\"use client\";\n\nimport { useEffect } from \"react\";\nimport { useFormState } from \"react-dom\";\nimport Link from \"next/link\""
  },
  {
    "path": "src/app/(auth)/signup/page.tsx",
    "chars": 417,
    "preview": "import { redirect } from \"next/navigation\";\nimport { Signup } from \"./signup\";\nimport { validateRequest } from \"@/lib/au"
  },
  {
    "path": "src/app/(auth)/signup/signup.tsx",
    "chars": 3249,
    "preview": "\"use client\";\n\nimport { useFormState } from \"react-dom\";\nimport Link from \"next/link\";\nimport { PasswordInput } from \"@/"
  },
  {
    "path": "src/app/(auth)/verify-email/page.tsx",
    "chars": 980,
    "preview": "import {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card\";\nimport { re"
  },
  {
    "path": "src/app/(auth)/verify-email/verify-code.tsx",
    "chars": 1934,
    "preview": "\"use client\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@radix-ui/react-label\";\nimport { use"
  },
  {
    "path": "src/app/(landing)/_components/copy-to-clipboard.tsx",
    "chars": 1161,
    "preview": "\"use client\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { "
  },
  {
    "path": "src/app/(landing)/_components/feature-icons.tsx",
    "chars": 24412,
    "preview": "import { forwardRef, type SVGProps } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst NextjsLight = forwardRef<SV"
  },
  {
    "path": "src/app/(landing)/_components/footer.tsx",
    "chars": 851,
    "preview": "import { ThemeToggle } from \"@/components/theme-toggle\";\nimport { CodeIcon } from \"@radix-ui/react-icons\";\n\nconst github"
  },
  {
    "path": "src/app/(landing)/_components/header.tsx",
    "chars": 2202,
    "preview": "import Link from \"next/link\";\nimport { RocketIcon } from \"@/components/icons\";\nimport { APP_TITLE } from \"@/lib/constant"
  },
  {
    "path": "src/app/(landing)/_components/hover-card.tsx",
    "chars": 1999,
    "preview": "\"use client\";\nimport { Card, CardDescription, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport React, { useRe"
  },
  {
    "path": "src/app/(landing)/layout.tsx",
    "chars": 566,
    "preview": "import { APP_TITLE } from \"@/lib/constants\";\nimport { type Metadata } from \"next\";\nimport { type ReactNode } from \"react"
  },
  {
    "path": "src/app/(landing)/page.tsx",
    "chars": 4778,
    "preview": "import { PlusIcon } from \"@/components/icons\";\nimport { Button } from \"@/components/ui/button\";\nimport { GitHubLogoIcon "
  },
  {
    "path": "src/app/(main)/_components/footer.tsx",
    "chars": 856,
    "preview": "import { ThemeToggle } from \"@/components/theme-toggle\";\nimport { CodeIcon } from \"@radix-ui/react-icons\";\n\nconst github"
  },
  {
    "path": "src/app/(main)/_components/header.tsx",
    "chars": 813,
    "preview": "import { UserDropdown } from \"@/app/(main)/_components/user-dropdown\";\nimport { RocketIcon } from \"@/components/icons\";\n"
  },
  {
    "path": "src/app/(main)/_components/user-dropdown.tsx",
    "chars": 3695,
    "preview": "\"use client\";\n\nimport { ExclamationTriangleIcon } from \"@/components/icons\";\nimport { LoadingButton } from \"@/components"
  },
  {
    "path": "src/app/(main)/account/page.tsx",
    "chars": 1025,
    "preview": "import { SubmitButton } from \"@/components/submit-button\";\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardFoot"
  },
  {
    "path": "src/app/(main)/dashboard/_components/dashboard-nav.tsx",
    "chars": 1132,
    "preview": "\"use client\";\n\nimport { CreditCard, FileTextIcon, GearIcon } from \"@/components/icons\";\nimport Link from \"next/link\";\nim"
  },
  {
    "path": "src/app/(main)/dashboard/_components/new-post.tsx",
    "chars": 2436,
    "preview": "\"use client\";\n\nimport { FilePlusIcon } from \"@/components/icons\";\nimport { Button } from \"@/components/ui/button\";\nimpor"
  },
  {
    "path": "src/app/(main)/dashboard/_components/post-card-skeleton.tsx",
    "chars": 724,
    "preview": "import { Card, CardContent, CardFooter, CardHeader } from \"@/components/ui/card\";\nimport { Skeleton } from \"@/components"
  },
  {
    "path": "src/app/(main)/dashboard/_components/post-card.tsx",
    "chars": 2921,
    "preview": "\"use client\";\n\nimport { Pencil2Icon, TrashIcon } from \"@/components/icons\";\nimport { Badge } from \"@/components/ui/badge"
  },
  {
    "path": "src/app/(main)/dashboard/_components/posts-skeleton.tsx",
    "chars": 280,
    "preview": "import { PostCardSkeleton } from \"./post-card-skeleton\";\n\nexport function PostsSkeleton() {\n  return (\n    <div classNam"
  },
  {
    "path": "src/app/(main)/dashboard/_components/posts.tsx",
    "chars": 1820,
    "preview": "\"use client\";\n\nimport { type RouterOutputs } from \"@/trpc/shared\";\nimport * as React from \"react\";\nimport { NewPost } fr"
  },
  {
    "path": "src/app/(main)/dashboard/_components/verificiation-warning.tsx",
    "chars": 1113,
    "preview": "import { ExclamationTriangleIcon } from \"@/components/icons\";\n\nimport { Alert, AlertDescription, AlertTitle } from \"@/co"
  },
  {
    "path": "src/app/(main)/dashboard/billing/_components/billing-skeleton.tsx",
    "chars": 1367,
    "preview": "import { Card, CardContent, CardFooter, CardHeader } from \"@/components/ui/card\";\nimport { Skeleton } from \"@/components"
  },
  {
    "path": "src/app/(main)/dashboard/billing/_components/billing.tsx",
    "chars": 3217,
    "preview": "import Link from \"next/link\";\n\nimport { CheckIcon } from \"@/components/icons\";\n\nimport { Button } from \"@/components/ui/"
  },
  {
    "path": "src/app/(main)/dashboard/billing/_components/manage-subscription-form.tsx",
    "chars": 1338,
    "preview": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport type { ManageSub"
  },
  {
    "path": "src/app/(main)/dashboard/billing/page.tsx",
    "chars": 2035,
    "preview": "import type { Metadata } from \"next\";\nimport { redirect } from \"next/navigation\";\n\nimport { ExclamationTriangleIcon } fr"
  },
  {
    "path": "src/app/(main)/dashboard/layout.tsx",
    "chars": 651,
    "preview": "import { DashboardNav } from \"./_components/dashboard-nav\";\nimport { VerificiationWarning } from \"./_components/verifici"
  },
  {
    "path": "src/app/(main)/dashboard/page.tsx",
    "chars": 1792,
    "preview": "import { env } from \"@/env\";\nimport { validateRequest } from \"@/lib/auth/validate-request\";\nimport { Paths } from \"@/lib"
  },
  {
    "path": "src/app/(main)/dashboard/settings/page.tsx",
    "chars": 752,
    "preview": "import type { Metadata } from \"next\";\nimport { redirect } from \"next/navigation\";\n\nimport { env } from \"@/env\";\nimport {"
  },
  {
    "path": "src/app/(main)/editor/[postId]/_components/post-editor.tsx",
    "chars": 4187,
    "preview": "\"use client\";\nimport { useRef } from \"react\";\nimport { type RouterOutputs } from \"@/trpc/shared\";\nimport { useForm } fro"
  },
  {
    "path": "src/app/(main)/editor/[postId]/_components/post-preview.tsx",
    "chars": 815,
    "preview": "import Markdown, { type Components } from \"react-markdown\";\nimport remarkGfm from \"remark-gfm\";\nimport rehypeRaw from \"r"
  },
  {
    "path": "src/app/(main)/editor/[postId]/page.tsx",
    "chars": 1020,
    "preview": "import React from \"react\";\nimport { api } from \"@/trpc/server\";\nimport { notFound, redirect } from \"next/navigation\";\nim"
  },
  {
    "path": "src/app/(main)/layout.tsx",
    "chars": 312,
    "preview": "import { type ReactNode } from \"react\";\nimport { Header } from \"./_components/header\";\nimport { Footer } from \"./_compon"
  },
  {
    "path": "src/app/api/trpc/[trpc]/route.ts",
    "chars": 998,
    "preview": "import { fetchRequestHandler } from \"@trpc/server/adapters/fetch\";\nimport { type NextRequest } from \"next/server\";\n\nimpo"
  },
  {
    "path": "src/app/api/webhooks/stripe/route.ts",
    "chars": 2754,
    "preview": "import { headers } from \"next/headers\";\n\nimport type Stripe from \"stripe\";\n\nimport { env } from \"@/env\";\nimport { stripe"
  },
  {
    "path": "src/app/icon.tsx",
    "chars": 3641,
    "preview": "import { ImageResponse } from \"next/og\";\n\n// Route segment config\nexport const runtime = \"edge\";\n\n// Image metadata\nexpo"
  },
  {
    "path": "src/app/layout.tsx",
    "chars": 1349,
    "preview": "import \"@/styles/globals.css\";\n\nimport { ThemeProvider } from \"@/components/theme-provider\";\nimport { Toaster } from \"@/"
  },
  {
    "path": "src/app/robots.ts",
    "chars": 262,
    "preview": "import { type MetadataRoute } from \"next\"\n\nimport { absoluteUrl } from \"@/lib/utils\"\n\nexport default function robots(): "
  },
  {
    "path": "src/app/sitemap.ts",
    "chars": 343,
    "preview": "import { type MetadataRoute } from \"next\";\n\nimport { absoluteUrl } from \"@/lib/utils\";\n\nexport default async function si"
  },
  {
    "path": "src/components/icons.tsx",
    "chars": 2542,
    "preview": "import { forwardRef, type SVGProps } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nconst AnimatedSpinner = forwardRef"
  },
  {
    "path": "src/components/loading-button.tsx",
    "chars": 963,
    "preview": "\"use client\";\n\nimport { forwardRef } from \"react\";\nimport { AnimatedSpinner } from \"@/components/icons\";\nimport { Button"
  },
  {
    "path": "src/components/password-input.tsx",
    "chars": 1415,
    "preview": "\"use client\";\n\nimport * as React from \"react\";\nimport { EyeOpenIcon, EyeCloseIcon } from \"@/components/icons\";\nimport { "
  },
  {
    "path": "src/components/responsive-dialog.tsx",
    "chars": 2732,
    "preview": "\"use client\";\n\nimport {\n  useState,\n  type ReactNode,\n  type Dispatch,\n  type SetStateAction,\n} from \"react\";\nimport { B"
  },
  {
    "path": "src/components/submit-button.tsx",
    "chars": 630,
    "preview": "\"use client\";\n\nimport { forwardRef } from \"react\";\nimport { useFormStatus } from \"react-dom\";\nimport { LoadingButton } f"
  },
  {
    "path": "src/components/theme-provider.tsx",
    "chars": 332,
    "preview": "\"use client\";\n\nimport * as React from \"react\";\nimport { ThemeProvider as NextThemesProvider } from \"next-themes\";\nimport"
  },
  {
    "path": "src/components/theme-toggle.tsx",
    "chars": 1236,
    "preview": "\"use client\";\n\nimport { useTheme } from \"next-themes\";\nimport { SunIcon, MoonIcon } from \"@/components/icons\";\nimport { "
  },
  {
    "path": "src/components/ui/alert-dialog.tsx",
    "chars": 4434,
    "preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as AlertDialogPrimitive from \"@radix-ui/react-alert-dialog\"\n\nimpor"
  },
  {
    "path": "src/components/ui/alert.tsx",
    "chars": 1584,
    "preview": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/"
  },
  {
    "path": "src/components/ui/badge.tsx",
    "chars": 1128,
    "preview": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/"
  },
  {
    "path": "src/components/ui/button.tsx",
    "chars": 1837,
    "preview": "import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class"
  },
  {
    "path": "src/components/ui/card.tsx",
    "chars": 1877,
    "preview": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Card = React.forwardRef<\n  HTMLDivElement,\n  Rea"
  },
  {
    "path": "src/components/ui/dialog.tsx",
    "chars": 3879,
    "preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\"\nimport { Cross1Ic"
  },
  {
    "path": "src/components/ui/drawer.tsx",
    "chars": 3021,
    "preview": "\"use client\"\n\nimport * as React from \"react\"\nimport { Drawer as DrawerPrimitive } from \"vaul\"\n\nimport { cn } from \"@/lib"
  },
  {
    "path": "src/components/ui/dropdown-menu.tsx",
    "chars": 7366,
    "preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\"\nimpo"
  },
  {
    "path": "src/components/ui/form.tsx",
    "chars": 4171,
    "preview": "import * as React from \"react\";\nimport type * as LabelPrimitive from \"@radix-ui/react-label\";\nimport { Slot } from \"@rad"
  },
  {
    "path": "src/components/ui/input.tsx",
    "chars": 801,
    "preview": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport interface InputProps\n  extends React.InputHTMLA"
  },
  {
    "path": "src/components/ui/label.tsx",
    "chars": 724,
    "preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as LabelPrimitive from \"@radix-ui/react-label\"\nimport { cva, type "
  },
  {
    "path": "src/components/ui/pagination.tsx",
    "chars": 2892,
    "preview": "import * as React from \"react\";\nimport Link from \"next/link\";\nimport {\n  ChevronLeftIcon,\n  ChevronRightIcon,\n  DotsHori"
  },
  {
    "path": "src/components/ui/skeleton.tsx",
    "chars": 261,
    "preview": "import { cn } from \"@/lib/utils\"\n\nfunction Skeleton({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) {"
  },
  {
    "path": "src/components/ui/sonner.tsx",
    "chars": 894,
    "preview": "\"use client\"\n\nimport { useTheme } from \"next-themes\"\nimport { Toaster as Sonner } from \"sonner\"\n\ntype ToasterProps = Rea"
  },
  {
    "path": "src/components/ui/tabs.tsx",
    "chars": 1891,
    "preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\"\n\nimport { cn } from \""
  },
  {
    "path": "src/components/ui/textarea.tsx",
    "chars": 772,
    "preview": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport interface TextareaProps\n  extends React.Textare"
  },
  {
    "path": "src/config/subscriptions.ts",
    "chars": 615,
    "preview": "import { env } from \"@/env\";\n\nexport interface SubscriptionPlan {\n  name: string;\n  description: string;\n  features: str"
  },
  {
    "path": "src/env.js",
    "chars": 2659,
    "preview": "import { createEnv } from \"@t3-oss/env-nextjs\";\nimport { z } from \"zod\";\n\nexport const env = createEnv({\n  /**\n   * Spec"
  },
  {
    "path": "src/lib/auth/actions.ts",
    "chars": 9189,
    "preview": "\"use server\";\n\n/* eslint @typescript-eslint/no-explicit-any:0, @typescript-eslint/prefer-optional-chain:0 */\n\nimport { z"
  },
  {
    "path": "src/lib/auth/index.ts",
    "chars": 1708,
    "preview": "import { Lucia, TimeSpan } from \"lucia\";\nimport { Discord } from \"arctic\";\nimport { DrizzlePostgreSQLAdapter } from \"@lu"
  },
  {
    "path": "src/lib/auth/validate-request.ts",
    "chars": 1180,
    "preview": "import { cache } from \"react\";\nimport { cookies } from \"next/headers\";\nimport type { Session, User } from \"lucia\";\nimpor"
  },
  {
    "path": "src/lib/constants.ts",
    "chars": 349,
    "preview": "export const APP_TITLE = \"Acme\";\nexport const DATABASE_PREFIX = \"acme\";\nexport const TEST_DB_PREFIX = \"test_acme\";\nexpor"
  },
  {
    "path": "src/lib/email/index.tsx",
    "chars": 2022,
    "preview": "import \"server-only\";\n\nimport { EmailVerificationTemplate } from \"./templates/email-verification\";\nimport { ResetPasswor"
  },
  {
    "path": "src/lib/email/templates/email-verification.tsx",
    "chars": 1926,
    "preview": "import { Body, Container, Head, Html, Preview, Section, Text } from \"@react-email/components\";\nimport { APP_TITLE } from"
  },
  {
    "path": "src/lib/email/templates/reset-password.tsx",
    "chars": 2251,
    "preview": "import { render } from \"@react-email/render\";\nimport {\n  Body,\n  Button,\n  Container,\n  Head,\n  Html,\n  Preview,\n  Secti"
  },
  {
    "path": "src/lib/fonts.ts",
    "chars": 175,
    "preview": "import \"@/styles/globals.css\";\n\nimport { Inter as FontSans } from \"next/font/google\";\n\nexport const fontSans = FontSans("
  },
  {
    "path": "src/lib/hooks/use-debounce.ts",
    "chars": 380,
    "preview": "import { useEffect, useState } from \"react\";\n\nexport function useDebounce<T>(value: T, delay: number) {\n  const [debounc"
  },
  {
    "path": "src/lib/hooks/use-media-query.ts",
    "chars": 465,
    "preview": "import * as React from \"react\";\n\nexport function useMediaQuery(query: string) {\n  const [value, setValue] = React.useSta"
  },
  {
    "path": "src/lib/logger.ts",
    "chars": 1703,
    "preview": "import { env } from \"@/env\";\nimport * as fs from \"fs\";\nimport * as path from \"path\";\n\nenum LogLevel {\n  DEBUG = \"DEBUG\","
  },
  {
    "path": "src/lib/stripe.ts",
    "chars": 166,
    "preview": "import { env } from \"@/env\";\nimport Stripe from \"stripe\";\n\nexport const stripe = new Stripe(env.STRIPE_API_KEY, {\n  apiV"
  },
  {
    "path": "src/lib/utils.ts",
    "chars": 1293,
    "preview": "import { env } from \"@/env\";\nimport { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nex"
  },
  {
    "path": "src/lib/validators/auth.ts",
    "chars": 906,
    "preview": "import { z } from \"zod\";\n\nexport const signupSchema = z.object({\n  email: z.string().email(\"Please enter a valid email\")"
  },
  {
    "path": "src/middleware.ts",
    "chars": 736,
    "preview": "// middleware.ts\nimport { verifyRequestOrigin } from \"lucia\";\nimport { NextResponse } from \"next/server\";\nimport type { "
  },
  {
    "path": "src/server/api/root.ts",
    "chars": 385,
    "preview": "import { postRouter } from \"./routers/post/post.procedure\";\nimport { stripeRouter } from \"./routers/stripe/stripe.proced"
  },
  {
    "path": "src/server/api/routers/post/post.input.ts",
    "chars": 1007,
    "preview": "import { z } from \"zod\";\n\nexport const listPostsSchema = z.object({\n  page: z.number().int().default(1),\n  perPage: z.nu"
  },
  {
    "path": "src/server/api/routers/post/post.procedure.ts",
    "chars": 983,
    "preview": "import { createTRPCRouter, protectedProcedure } from \"../../trpc\";\nimport * as inputs from \"./post.input\";\nimport * as s"
  },
  {
    "path": "src/server/api/routers/post/post.service.ts",
    "chars": 2289,
    "preview": "import { generateId } from \"lucia\";\nimport type { ProtectedTRPCContext } from \"../../trpc\";\nimport type {\n  CreatePostIn"
  },
  {
    "path": "src/server/api/routers/stripe/stripe.input.ts",
    "chars": 325,
    "preview": "import { z } from \"zod\";\n\nexport const manageSubscriptionSchema = z.object({\n  stripePriceId: z.string(),\n  stripeCustom"
  },
  {
    "path": "src/server/api/routers/stripe/stripe.procedure.ts",
    "chars": 522,
    "preview": "import { createTRPCRouter, protectedProcedure } from \"../../trpc\";\nimport * as services from \"./stripe.service\";\nimport "
  },
  {
    "path": "src/server/api/routers/stripe/stripe.service.ts",
    "chars": 3688,
    "preview": "import { freePlan, proPlan, subscriptionPlans } from \"@/config/subscriptions\";\nimport type { ProtectedTRPCContext } from"
  },
  {
    "path": "src/server/api/routers/user/user.procedure.ts",
    "chars": 173,
    "preview": "import { protectedProcedure, createTRPCRouter } from \"../../trpc\";\n\nexport const userRouter = createTRPCRouter({\n  get: "
  },
  {
    "path": "src/server/api/trpc.ts",
    "chars": 3305,
    "preview": "/**\n * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS:\n * 1. You want to modify request context (see Part 1).\n * 2. Y"
  },
  {
    "path": "src/server/db/index.ts",
    "chars": 419,
    "preview": "import { drizzle } from \"drizzle-orm/postgres-js\";\nimport postgres from \"postgres\";\nimport { env } from \"@/env\";\nimport "
  },
  {
    "path": "src/server/db/schema.ts",
    "chars": 3598,
    "preview": "import { relations } from \"drizzle-orm\";\nimport {\n  pgTableCreator,\n  serial,\n  boolean,\n  index,\n  text,\n  timestamp,\n "
  },
  {
    "path": "src/styles/globals.css",
    "chars": 2209,
    "preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer base {\n  :root {\n    --background: 0 0% 98%;\n    --fo"
  },
  {
    "path": "src/trpc/react.tsx",
    "chars": 1118,
    "preview": "\"use client\";\n\nimport { QueryClient, QueryClientProvider } from \"@tanstack/react-query\";\nimport { loggerLink, unstable_h"
  },
  {
    "path": "src/trpc/server.ts",
    "chars": 1926,
    "preview": "import \"server-only\";\n\nimport {\n  createTRPCProxyClient,\n  loggerLink,\n  TRPCClientError,\n} from \"@trpc/client\";\nimport "
  },
  {
    "path": "src/trpc/shared.ts",
    "chars": 813,
    "preview": "import { type inferRouterInputs, type inferRouterOutputs } from \"@trpc/server\";\nimport superjson from \"superjson\";\n\nimpo"
  },
  {
    "path": "tailwind.config.ts",
    "chars": 2235,
    "preview": "import { type Config } from \"tailwindcss\";\nimport { fontFamily } from \"tailwindcss/defaultTheme\";\n\nexport default {\n  da"
  },
  {
    "path": "tests/e2e/auth-with-credential.spec.ts",
    "chars": 1659,
    "preview": "import { db } from \"@/server/db\";\nimport { users } from \"@/server/db/schema\";\nimport { test, expect } from \"@playwright/"
  },
  {
    "path": "tests/e2e/utils.ts",
    "chars": 489,
    "preview": "export const testUser = {\n  name: \"Test User\",\n  email: \"test@saasykits.com\",\n  password: \"testPass123\",\n};\n\nexport func"
  },
  {
    "path": "tsconfig.json",
    "chars": 857,
    "preview": "{\n  \"compilerOptions\": {\n    /* Base Options: */\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true,\n    \"target\": \"e"
  }
]

About this extraction

This page contains the full source code of the iamtouha/next-lucia-auth GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 125 files (213.5 KB), approximately 65.3k tokens, and a symbol index with 123 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!