[
  {
    "path": ".gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n.yarn/install-state.gz\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# local env files\n.env\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n.vscode\n\n# Docker\npostgres_data/\n.env*.local\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Vercel\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Next.js SaaS Starter\n\nThis is a starter template for building a SaaS application using **Next.js** with support for authentication, Stripe integration for payments, and a dashboard for logged-in users.\n\n**Demo: [https://next-saas-start.vercel.app/](https://next-saas-start.vercel.app/)**\n\n## Features\n\n- Marketing landing page (`/`) with animated Terminal element\n- Pricing page (`/pricing`) which connects to Stripe Checkout\n- Dashboard pages with CRUD operations on users/teams\n- Basic RBAC with Owner and Member roles\n- Subscription management with Stripe Customer Portal\n- Email/password authentication with JWTs stored to cookies\n- Global middleware to protect logged-in routes\n- Local middleware to protect Server Actions or validate Zod schemas\n- Activity logging system for any user events\n\n## Tech Stack\n\n- **Framework**: [Next.js](https://nextjs.org/)\n- **Database**: [Postgres](https://www.postgresql.org/)\n- **ORM**: [Drizzle](https://orm.drizzle.team/)\n- **Payments**: [Stripe](https://stripe.com/)\n- **UI Library**: [shadcn/ui](https://ui.shadcn.com/)\n\n## Getting Started\n\n```bash\ngit clone https://github.com/nextjs/saas-starter\ncd saas-starter\npnpm install\n```\n\n## Running Locally\n\n[Install](https://docs.stripe.com/stripe-cli) and log in to your Stripe account:\n\n```bash\nstripe login\n```\n\nUse the included setup script to create your `.env` file:\n\n```bash\npnpm db:setup\n```\n\nRun the database migrations and seed the database with a default user and team:\n\n```bash\npnpm db:migrate\npnpm db:seed\n```\n\nThis will create the following user and team:\n\n- User: `test@test.com`\n- Password: `admin123`\n\nYou can also create new users through the `/sign-up` route.\n\nFinally, run the Next.js development server:\n\n```bash\npnpm dev\n```\n\nOpen [http://localhost:3000](http://localhost:3000) in your browser to see the app in action.\n\nYou can listen for Stripe webhooks locally through their CLI to handle subscription change events:\n\n```bash\nstripe listen --forward-to localhost:3000/api/stripe/webhook\n```\n\n## Testing Payments\n\nTo test Stripe payments, use the following test card details:\n\n- Card Number: `4242 4242 4242 4242`\n- Expiration: Any future date\n- CVC: Any 3-digit number\n\n## Going to Production\n\nWhen you're ready to deploy your SaaS application to production, follow these steps:\n\n### Set up a production Stripe webhook\n\n1. Go to the Stripe Dashboard and create a new webhook for your production environment.\n2. Set the endpoint URL to your production API route (e.g., `https://yourdomain.com/api/stripe/webhook`).\n3. Select the events you want to listen for (e.g., `checkout.session.completed`, `customer.subscription.updated`).\n\n### Deploy to Vercel\n\n1. Push your code to a GitHub repository.\n2. Connect your repository to [Vercel](https://vercel.com/) and deploy it.\n3. Follow the Vercel deployment process, which will guide you through setting up your project.\n\n### Add environment variables\n\nIn your Vercel project settings (or during deployment), add all the necessary environment variables. Make sure to update the values for the production environment, including:\n\n1. `BASE_URL`: Set this to your production domain.\n2. `STRIPE_SECRET_KEY`: Use your Stripe secret key for the production environment.\n3. `STRIPE_WEBHOOK_SECRET`: Use the webhook secret from the production webhook you created in step 1.\n4. `POSTGRES_URL`: Set this to your production database URL.\n5. `AUTH_SECRET`: Set this to a random string. `openssl rand -base64 32` will generate one.\n\n## Other Templates\n\nWhile this template is intentionally minimal and to be used as a learning resource, there are other paid versions in the community which are more full-featured:\n\n- https://achromatic.dev\n- https://shipfa.st\n- https://makerkit.dev\n- https://zerotoshipped.com\n- https://turbostarter.dev\n"
  },
  {
    "path": "app/(dashboard)/dashboard/activity/loading.tsx",
    "content": "import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';\n\nexport default function ActivityPageSkeleton() {\n  return (\n    <section className=\"flex-1 p-4 lg:p-8\">\n      <h1 className=\"text-lg lg:text-2xl font-medium text-gray-900 mb-6\">\n        Activity Log\n      </h1>\n      <Card>\n        <CardHeader>\n          <CardTitle>Recent Activity</CardTitle>\n        </CardHeader>\n        <CardContent className=\"min-h-[88px]\" />\n      </Card>\n    </section>\n  );\n}\n"
  },
  {
    "path": "app/(dashboard)/dashboard/activity/page.tsx",
    "content": "import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';\nimport {\n  Settings,\n  LogOut,\n  UserPlus,\n  Lock,\n  UserCog,\n  AlertCircle,\n  UserMinus,\n  Mail,\n  CheckCircle,\n  type LucideIcon,\n} from 'lucide-react';\nimport { ActivityType } from '@/lib/db/schema';\nimport { getActivityLogs } from '@/lib/db/queries';\n\nconst iconMap: Record<ActivityType, LucideIcon> = {\n  [ActivityType.SIGN_UP]: UserPlus,\n  [ActivityType.SIGN_IN]: UserCog,\n  [ActivityType.SIGN_OUT]: LogOut,\n  [ActivityType.UPDATE_PASSWORD]: Lock,\n  [ActivityType.DELETE_ACCOUNT]: UserMinus,\n  [ActivityType.UPDATE_ACCOUNT]: Settings,\n  [ActivityType.CREATE_TEAM]: UserPlus,\n  [ActivityType.REMOVE_TEAM_MEMBER]: UserMinus,\n  [ActivityType.INVITE_TEAM_MEMBER]: Mail,\n  [ActivityType.ACCEPT_INVITATION]: CheckCircle,\n};\n\nfunction getRelativeTime(date: Date) {\n  const now = new Date();\n  const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);\n\n  if (diffInSeconds < 60) return 'just now';\n  if (diffInSeconds < 3600)\n    return `${Math.floor(diffInSeconds / 60)} minutes ago`;\n  if (diffInSeconds < 86400)\n    return `${Math.floor(diffInSeconds / 3600)} hours ago`;\n  if (diffInSeconds < 604800)\n    return `${Math.floor(diffInSeconds / 86400)} days ago`;\n  return date.toLocaleDateString();\n}\n\nfunction formatAction(action: ActivityType): string {\n  switch (action) {\n    case ActivityType.SIGN_UP:\n      return 'You signed up';\n    case ActivityType.SIGN_IN:\n      return 'You signed in';\n    case ActivityType.SIGN_OUT:\n      return 'You signed out';\n    case ActivityType.UPDATE_PASSWORD:\n      return 'You changed your password';\n    case ActivityType.DELETE_ACCOUNT:\n      return 'You deleted your account';\n    case ActivityType.UPDATE_ACCOUNT:\n      return 'You updated your account';\n    case ActivityType.CREATE_TEAM:\n      return 'You created a new team';\n    case ActivityType.REMOVE_TEAM_MEMBER:\n      return 'You removed a team member';\n    case ActivityType.INVITE_TEAM_MEMBER:\n      return 'You invited a team member';\n    case ActivityType.ACCEPT_INVITATION:\n      return 'You accepted an invitation';\n    default:\n      return 'Unknown action occurred';\n  }\n}\n\nexport default async function ActivityPage() {\n  const logs = await getActivityLogs();\n\n  return (\n    <section className=\"flex-1 p-4 lg:p-8\">\n      <h1 className=\"text-lg lg:text-2xl font-medium text-gray-900 mb-6\">\n        Activity Log\n      </h1>\n      <Card>\n        <CardHeader>\n          <CardTitle>Recent Activity</CardTitle>\n        </CardHeader>\n        <CardContent>\n          {logs.length > 0 ? (\n            <ul className=\"space-y-4\">\n              {logs.map((log) => {\n                const Icon = iconMap[log.action as ActivityType] || Settings;\n                const formattedAction = formatAction(\n                  log.action as ActivityType\n                );\n\n                return (\n                  <li key={log.id} className=\"flex items-center space-x-4\">\n                    <div className=\"bg-orange-100 rounded-full p-2\">\n                      <Icon className=\"w-5 h-5 text-orange-600\" />\n                    </div>\n                    <div className=\"flex-1\">\n                      <p className=\"text-sm font-medium text-gray-900\">\n                        {formattedAction}\n                        {log.ipAddress && ` from IP ${log.ipAddress}`}\n                      </p>\n                      <p className=\"text-xs text-gray-500\">\n                        {getRelativeTime(new Date(log.timestamp))}\n                      </p>\n                    </div>\n                  </li>\n                );\n              })}\n            </ul>\n          ) : (\n            <div className=\"flex flex-col items-center justify-center text-center py-12\">\n              <AlertCircle className=\"h-12 w-12 text-orange-500 mb-4\" />\n              <h3 className=\"text-lg font-semibold text-gray-900 mb-2\">\n                No activity yet\n              </h3>\n              <p className=\"text-sm text-gray-500 max-w-sm\">\n                When you perform actions like signing in or updating your\n                account, they'll appear here.\n              </p>\n            </div>\n          )}\n        </CardContent>\n      </Card>\n    </section>\n  );\n}\n"
  },
  {
    "path": "app/(dashboard)/dashboard/general/page.tsx",
    "content": "'use client';\n\nimport { useActionState } from 'react';\nimport { Button } from '@/components/ui/button';\nimport { Input } from '@/components/ui/input';\nimport { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';\nimport { Label } from '@/components/ui/label';\nimport { Loader2 } from 'lucide-react';\nimport { updateAccount } from '@/app/(login)/actions';\nimport { User } from '@/lib/db/schema';\nimport useSWR from 'swr';\nimport { Suspense } from 'react';\n\nconst fetcher = (url: string) => fetch(url).then((res) => res.json());\n\ntype ActionState = {\n  name?: string;\n  error?: string;\n  success?: string;\n};\n\ntype AccountFormProps = {\n  state: ActionState;\n  nameValue?: string;\n  emailValue?: string;\n};\n\nfunction AccountForm({\n  state,\n  nameValue = '',\n  emailValue = ''\n}: AccountFormProps) {\n  return (\n    <>\n      <div>\n        <Label htmlFor=\"name\" className=\"mb-2\">\n          Name\n        </Label>\n        <Input\n          id=\"name\"\n          name=\"name\"\n          placeholder=\"Enter your name\"\n          defaultValue={state.name || nameValue}\n          required\n        />\n      </div>\n      <div>\n        <Label htmlFor=\"email\" className=\"mb-2\">\n          Email\n        </Label>\n        <Input\n          id=\"email\"\n          name=\"email\"\n          type=\"email\"\n          placeholder=\"Enter your email\"\n          defaultValue={emailValue}\n          required\n        />\n      </div>\n    </>\n  );\n}\n\nfunction AccountFormWithData({ state }: { state: ActionState }) {\n  const { data: user } = useSWR<User>('/api/user', fetcher);\n  return (\n    <AccountForm\n      state={state}\n      nameValue={user?.name ?? ''}\n      emailValue={user?.email ?? ''}\n    />\n  );\n}\n\nexport default function GeneralPage() {\n  const [state, formAction, isPending] = useActionState<ActionState, FormData>(\n    updateAccount,\n    {}\n  );\n\n  return (\n    <section className=\"flex-1 p-4 lg:p-8\">\n      <h1 className=\"text-lg lg:text-2xl font-medium text-gray-900 mb-6\">\n        General Settings\n      </h1>\n\n      <Card>\n        <CardHeader>\n          <CardTitle>Account Information</CardTitle>\n        </CardHeader>\n        <CardContent>\n          <form className=\"space-y-4\" action={formAction}>\n            <Suspense fallback={<AccountForm state={state} />}>\n              <AccountFormWithData state={state} />\n            </Suspense>\n            {state.error && (\n              <p className=\"text-red-500 text-sm\">{state.error}</p>\n            )}\n            {state.success && (\n              <p className=\"text-green-500 text-sm\">{state.success}</p>\n            )}\n            <Button\n              type=\"submit\"\n              className=\"bg-orange-500 hover:bg-orange-600 text-white\"\n              disabled={isPending}\n            >\n              {isPending ? (\n                <>\n                  <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n                  Saving...\n                </>\n              ) : (\n                'Save Changes'\n              )}\n            </Button>\n          </form>\n        </CardContent>\n      </Card>\n    </section>\n  );\n}\n"
  },
  {
    "path": "app/(dashboard)/dashboard/layout.tsx",
    "content": "'use client';\n\nimport { useState } from 'react';\nimport Link from 'next/link';\nimport { usePathname } from 'next/navigation';\nimport { Button } from '@/components/ui/button';\nimport { Users, Settings, Shield, Activity, Menu } from 'lucide-react';\n\nexport default function DashboardLayout({\n  children\n}: {\n  children: React.ReactNode;\n}) {\n  const pathname = usePathname();\n  const [isSidebarOpen, setIsSidebarOpen] = useState(false);\n\n  const navItems = [\n    { href: '/dashboard', icon: Users, label: 'Team' },\n    { href: '/dashboard/general', icon: Settings, label: 'General' },\n    { href: '/dashboard/activity', icon: Activity, label: 'Activity' },\n    { href: '/dashboard/security', icon: Shield, label: 'Security' }\n  ];\n\n  return (\n    <div className=\"flex flex-col min-h-[calc(100dvh-68px)] max-w-7xl mx-auto w-full\">\n      {/* Mobile header */}\n      <div className=\"lg:hidden flex items-center justify-between bg-white border-b border-gray-200 p-4\">\n        <div className=\"flex items-center\">\n          <span className=\"font-medium\">Settings</span>\n        </div>\n        <Button\n          className=\"-mr-3\"\n          variant=\"ghost\"\n          onClick={() => setIsSidebarOpen(!isSidebarOpen)}\n        >\n          <Menu className=\"h-6 w-6\" />\n          <span className=\"sr-only\">Toggle sidebar</span>\n        </Button>\n      </div>\n\n      <div className=\"flex flex-1 overflow-hidden h-full\">\n        {/* Sidebar */}\n        <aside\n          className={`w-64 bg-white lg:bg-gray-50 border-r border-gray-200 lg:block ${\n            isSidebarOpen ? 'block' : 'hidden'\n          } lg:relative absolute inset-y-0 left-0 z-40 transform transition-transform duration-300 ease-in-out lg:translate-x-0 ${\n            isSidebarOpen ? 'translate-x-0' : '-translate-x-full'\n          }`}\n        >\n          <nav className=\"h-full overflow-y-auto p-4\">\n            {navItems.map((item) => (\n              <Link key={item.href} href={item.href} passHref>\n                <Button\n                  variant={pathname === item.href ? 'secondary' : 'ghost'}\n                  className={`shadow-none my-1 w-full justify-start ${\n                    pathname === item.href ? 'bg-gray-100' : ''\n                  }`}\n                  onClick={() => setIsSidebarOpen(false)}\n                >\n                  <item.icon className=\"h-4 w-4\" />\n                  {item.label}\n                </Button>\n              </Link>\n            ))}\n          </nav>\n        </aside>\n\n        {/* Main content */}\n        <main className=\"flex-1 overflow-y-auto p-0 lg:p-4\">{children}</main>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/(dashboard)/dashboard/page.tsx",
    "content": "'use client';\n\nimport { Button } from '@/components/ui/button';\nimport { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';\nimport {\n  Card,\n  CardContent,\n  CardHeader,\n  CardTitle,\n  CardFooter\n} from '@/components/ui/card';\nimport { customerPortalAction } from '@/lib/payments/actions';\nimport { useActionState } from 'react';\nimport { TeamDataWithMembers, User } from '@/lib/db/schema';\nimport { removeTeamMember, inviteTeamMember } from '@/app/(login)/actions';\nimport useSWR from 'swr';\nimport { Suspense } from 'react';\nimport { Input } from '@/components/ui/input';\nimport { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';\nimport { Label } from '@/components/ui/label';\nimport { Loader2, PlusCircle } from 'lucide-react';\n\ntype ActionState = {\n  error?: string;\n  success?: string;\n};\n\nconst fetcher = (url: string) => fetch(url).then((res) => res.json());\n\nfunction SubscriptionSkeleton() {\n  return (\n    <Card className=\"mb-8 h-[140px]\">\n      <CardHeader>\n        <CardTitle>Team Subscription</CardTitle>\n      </CardHeader>\n    </Card>\n  );\n}\n\nfunction ManageSubscription() {\n  const { data: teamData } = useSWR<TeamDataWithMembers>('/api/team', fetcher);\n\n  return (\n    <Card className=\"mb-8\">\n      <CardHeader>\n        <CardTitle>Team Subscription</CardTitle>\n      </CardHeader>\n      <CardContent>\n        <div className=\"space-y-4\">\n          <div className=\"flex flex-col sm:flex-row justify-between items-start sm:items-center\">\n            <div className=\"mb-4 sm:mb-0\">\n              <p className=\"font-medium\">\n                Current Plan: {teamData?.planName || 'Free'}\n              </p>\n              <p className=\"text-sm text-muted-foreground\">\n                {teamData?.subscriptionStatus === 'active'\n                  ? 'Billed monthly'\n                  : teamData?.subscriptionStatus === 'trialing'\n                  ? 'Trial period'\n                  : 'No active subscription'}\n              </p>\n            </div>\n            <form action={customerPortalAction}>\n              <Button type=\"submit\" variant=\"outline\">\n                Manage Subscription\n              </Button>\n            </form>\n          </div>\n        </div>\n      </CardContent>\n    </Card>\n  );\n}\n\nfunction TeamMembersSkeleton() {\n  return (\n    <Card className=\"mb-8 h-[140px]\">\n      <CardHeader>\n        <CardTitle>Team Members</CardTitle>\n      </CardHeader>\n      <CardContent>\n        <div className=\"animate-pulse space-y-4 mt-1\">\n          <div className=\"flex items-center space-x-4\">\n            <div className=\"size-8 rounded-full bg-gray-200\"></div>\n            <div className=\"space-y-2\">\n              <div className=\"h-4 w-32 bg-gray-200 rounded\"></div>\n              <div className=\"h-3 w-14 bg-gray-200 rounded\"></div>\n            </div>\n          </div>\n        </div>\n      </CardContent>\n    </Card>\n  );\n}\n\nfunction TeamMembers() {\n  const { data: teamData } = useSWR<TeamDataWithMembers>('/api/team', fetcher);\n  const [removeState, removeAction, isRemovePending] = useActionState<\n    ActionState,\n    FormData\n  >(removeTeamMember, {});\n\n  const getUserDisplayName = (user: Pick<User, 'id' | 'name' | 'email'>) => {\n    return user.name || user.email || 'Unknown User';\n  };\n\n  if (!teamData?.teamMembers?.length) {\n    return (\n      <Card className=\"mb-8\">\n        <CardHeader>\n          <CardTitle>Team Members</CardTitle>\n        </CardHeader>\n        <CardContent>\n          <p className=\"text-muted-foreground\">No team members yet.</p>\n        </CardContent>\n      </Card>\n    );\n  }\n\n  return (\n    <Card className=\"mb-8\">\n      <CardHeader>\n        <CardTitle>Team Members</CardTitle>\n      </CardHeader>\n      <CardContent>\n        <ul className=\"space-y-4\">\n          {teamData.teamMembers.map((member, index) => (\n            <li key={member.id} className=\"flex items-center justify-between\">\n              <div className=\"flex items-center space-x-4\">\n                <Avatar>\n                  {/* \n                    This app doesn't save profile images, but here\n                    is how you'd show them:\n\n                    <AvatarImage\n                      src={member.user.image || ''}\n                      alt={getUserDisplayName(member.user)}\n                    />\n                  */}\n                  <AvatarFallback>\n                    {getUserDisplayName(member.user)\n                      .split(' ')\n                      .map((n) => n[0])\n                      .join('')}\n                  </AvatarFallback>\n                </Avatar>\n                <div>\n                  <p className=\"font-medium\">\n                    {getUserDisplayName(member.user)}\n                  </p>\n                  <p className=\"text-sm text-muted-foreground capitalize\">\n                    {member.role}\n                  </p>\n                </div>\n              </div>\n              {index > 1 ? (\n                <form action={removeAction}>\n                  <input type=\"hidden\" name=\"memberId\" value={member.id} />\n                  <Button\n                    type=\"submit\"\n                    variant=\"outline\"\n                    size=\"sm\"\n                    disabled={isRemovePending}\n                  >\n                    {isRemovePending ? 'Removing...' : 'Remove'}\n                  </Button>\n                </form>\n              ) : null}\n            </li>\n          ))}\n        </ul>\n        {removeState?.error && (\n          <p className=\"text-red-500 mt-4\">{removeState.error}</p>\n        )}\n      </CardContent>\n    </Card>\n  );\n}\n\nfunction InviteTeamMemberSkeleton() {\n  return (\n    <Card className=\"h-[260px]\">\n      <CardHeader>\n        <CardTitle>Invite Team Member</CardTitle>\n      </CardHeader>\n    </Card>\n  );\n}\n\nfunction InviteTeamMember() {\n  const { data: user } = useSWR<User>('/api/user', fetcher);\n  const isOwner = user?.role === 'owner';\n  const [inviteState, inviteAction, isInvitePending] = useActionState<\n    ActionState,\n    FormData\n  >(inviteTeamMember, {});\n\n  return (\n    <Card>\n      <CardHeader>\n        <CardTitle>Invite Team Member</CardTitle>\n      </CardHeader>\n      <CardContent>\n        <form action={inviteAction} className=\"space-y-4\">\n          <div>\n            <Label htmlFor=\"email\" className=\"mb-2\">\n              Email\n            </Label>\n            <Input\n              id=\"email\"\n              name=\"email\"\n              type=\"email\"\n              placeholder=\"Enter email\"\n              required\n              disabled={!isOwner}\n            />\n          </div>\n          <div>\n            <Label>Role</Label>\n            <RadioGroup\n              defaultValue=\"member\"\n              name=\"role\"\n              className=\"flex space-x-4\"\n              disabled={!isOwner}\n            >\n              <div className=\"flex items-center space-x-2 mt-2\">\n                <RadioGroupItem value=\"member\" id=\"member\" />\n                <Label htmlFor=\"member\">Member</Label>\n              </div>\n              <div className=\"flex items-center space-x-2 mt-2\">\n                <RadioGroupItem value=\"owner\" id=\"owner\" />\n                <Label htmlFor=\"owner\">Owner</Label>\n              </div>\n            </RadioGroup>\n          </div>\n          {inviteState?.error && (\n            <p className=\"text-red-500\">{inviteState.error}</p>\n          )}\n          {inviteState?.success && (\n            <p className=\"text-green-500\">{inviteState.success}</p>\n          )}\n          <Button\n            type=\"submit\"\n            className=\"bg-orange-500 hover:bg-orange-600 text-white\"\n            disabled={isInvitePending || !isOwner}\n          >\n            {isInvitePending ? (\n              <>\n                <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n                Inviting...\n              </>\n            ) : (\n              <>\n                <PlusCircle className=\"mr-2 h-4 w-4\" />\n                Invite Member\n              </>\n            )}\n          </Button>\n        </form>\n      </CardContent>\n      {!isOwner && (\n        <CardFooter>\n          <p className=\"text-sm text-muted-foreground\">\n            You must be a team owner to invite new members.\n          </p>\n        </CardFooter>\n      )}\n    </Card>\n  );\n}\n\nexport default function SettingsPage() {\n  return (\n    <section className=\"flex-1 p-4 lg:p-8\">\n      <h1 className=\"text-lg lg:text-2xl font-medium mb-6\">Team Settings</h1>\n      <Suspense fallback={<SubscriptionSkeleton />}>\n        <ManageSubscription />\n      </Suspense>\n      <Suspense fallback={<TeamMembersSkeleton />}>\n        <TeamMembers />\n      </Suspense>\n      <Suspense fallback={<InviteTeamMemberSkeleton />}>\n        <InviteTeamMember />\n      </Suspense>\n    </section>\n  );\n}\n"
  },
  {
    "path": "app/(dashboard)/dashboard/security/page.tsx",
    "content": "'use client';\n\nimport { Button } from '@/components/ui/button';\nimport { Input } from '@/components/ui/input';\nimport { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';\nimport { Label } from '@/components/ui/label';\nimport { Lock, Trash2, Loader2 } from 'lucide-react';\nimport { useActionState } from 'react';\nimport { updatePassword, deleteAccount } from '@/app/(login)/actions';\n\ntype PasswordState = {\n  currentPassword?: string;\n  newPassword?: string;\n  confirmPassword?: string;\n  error?: string;\n  success?: string;\n};\n\ntype DeleteState = {\n  password?: string;\n  error?: string;\n  success?: string;\n};\n\nexport default function SecurityPage() {\n  const [passwordState, passwordAction, isPasswordPending] = useActionState<\n    PasswordState,\n    FormData\n  >(updatePassword, {});\n\n  const [deleteState, deleteAction, isDeletePending] = useActionState<\n    DeleteState,\n    FormData\n  >(deleteAccount, {});\n\n  return (\n    <section className=\"flex-1 p-4 lg:p-8\">\n      <h1 className=\"text-lg lg:text-2xl font-medium bold text-gray-900 mb-6\">\n        Security Settings\n      </h1>\n      <Card className=\"mb-8\">\n        <CardHeader>\n          <CardTitle>Password</CardTitle>\n        </CardHeader>\n        <CardContent>\n          <form className=\"space-y-4\" action={passwordAction}>\n            <div>\n              <Label htmlFor=\"current-password\" className=\"mb-2\">\n                Current Password\n              </Label>\n              <Input\n                id=\"current-password\"\n                name=\"currentPassword\"\n                type=\"password\"\n                autoComplete=\"current-password\"\n                required\n                minLength={8}\n                maxLength={100}\n                defaultValue={passwordState.currentPassword}\n              />\n            </div>\n            <div>\n              <Label htmlFor=\"new-password\" className=\"mb-2\">\n                New Password\n              </Label>\n              <Input\n                id=\"new-password\"\n                name=\"newPassword\"\n                type=\"password\"\n                autoComplete=\"new-password\"\n                required\n                minLength={8}\n                maxLength={100}\n                defaultValue={passwordState.newPassword}\n              />\n            </div>\n            <div>\n              <Label htmlFor=\"confirm-password\" className=\"mb-2\">\n                Confirm New Password\n              </Label>\n              <Input\n                id=\"confirm-password\"\n                name=\"confirmPassword\"\n                type=\"password\"\n                required\n                minLength={8}\n                maxLength={100}\n                defaultValue={passwordState.confirmPassword}\n              />\n            </div>\n            {passwordState.error && (\n              <p className=\"text-red-500 text-sm\">{passwordState.error}</p>\n            )}\n            {passwordState.success && (\n              <p className=\"text-green-500 text-sm\">{passwordState.success}</p>\n            )}\n            <Button\n              type=\"submit\"\n              className=\"bg-orange-500 hover:bg-orange-600 text-white\"\n              disabled={isPasswordPending}\n            >\n              {isPasswordPending ? (\n                <>\n                  <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n                  Updating...\n                </>\n              ) : (\n                <>\n                  <Lock className=\"mr-2 h-4 w-4\" />\n                  Update Password\n                </>\n              )}\n            </Button>\n          </form>\n        </CardContent>\n      </Card>\n\n      <Card>\n        <CardHeader>\n          <CardTitle>Delete Account</CardTitle>\n        </CardHeader>\n        <CardContent>\n          <p className=\"text-sm text-gray-500 mb-4\">\n            Account deletion is non-reversable. Please proceed with caution.\n          </p>\n          <form action={deleteAction} className=\"space-y-4\">\n            <div>\n              <Label htmlFor=\"delete-password\" className=\"mb-2\">\n                Confirm Password\n              </Label>\n              <Input\n                id=\"delete-password\"\n                name=\"password\"\n                type=\"password\"\n                required\n                minLength={8}\n                maxLength={100}\n                defaultValue={deleteState.password}\n              />\n            </div>\n            {deleteState.error && (\n              <p className=\"text-red-500 text-sm\">{deleteState.error}</p>\n            )}\n            <Button\n              type=\"submit\"\n              variant=\"destructive\"\n              className=\"bg-red-600 hover:bg-red-700\"\n              disabled={isDeletePending}\n            >\n              {isDeletePending ? (\n                <>\n                  <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n                  Deleting...\n                </>\n              ) : (\n                <>\n                  <Trash2 className=\"mr-2 h-4 w-4\" />\n                  Delete Account\n                </>\n              )}\n            </Button>\n          </form>\n        </CardContent>\n      </Card>\n    </section>\n  );\n}\n"
  },
  {
    "path": "app/(dashboard)/layout.tsx",
    "content": "'use client';\n\nimport Link from 'next/link';\nimport { use, useState, Suspense } from 'react';\nimport { Button } from '@/components/ui/button';\nimport { CircleIcon, Home, LogOut } from 'lucide-react';\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger\n} from '@/components/ui/dropdown-menu';\nimport { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';\nimport { signOut } from '@/app/(login)/actions';\nimport { useRouter } from 'next/navigation';\nimport { User } from '@/lib/db/schema';\nimport useSWR, { mutate } from 'swr';\n\nconst fetcher = (url: string) => fetch(url).then((res) => res.json());\n\nfunction UserMenu() {\n  const [isMenuOpen, setIsMenuOpen] = useState(false);\n  const { data: user } = useSWR<User>('/api/user', fetcher);\n  const router = useRouter();\n\n  async function handleSignOut() {\n    await signOut();\n    mutate('/api/user');\n    router.push('/');\n  }\n\n  if (!user) {\n    return (\n      <>\n        <Link\n          href=\"/pricing\"\n          className=\"text-sm font-medium text-gray-700 hover:text-gray-900\"\n        >\n          Pricing\n        </Link>\n        <Button asChild className=\"rounded-full\">\n          <Link href=\"/sign-up\">Sign Up</Link>\n        </Button>\n      </>\n    );\n  }\n\n  return (\n    <DropdownMenu open={isMenuOpen} onOpenChange={setIsMenuOpen}>\n      <DropdownMenuTrigger>\n        <Avatar className=\"cursor-pointer size-9\">\n          <AvatarImage alt={user.name || ''} />\n          <AvatarFallback>\n            {user.email\n              .split(' ')\n              .map((n) => n[0])\n              .join('')}\n          </AvatarFallback>\n        </Avatar>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"end\" className=\"flex flex-col gap-1\">\n        <DropdownMenuItem className=\"cursor-pointer\">\n          <Link href=\"/dashboard\" className=\"flex w-full items-center\">\n            <Home className=\"mr-2 h-4 w-4\" />\n            <span>Dashboard</span>\n          </Link>\n        </DropdownMenuItem>\n        <form action={handleSignOut} className=\"w-full\">\n          <button type=\"submit\" className=\"flex w-full\">\n            <DropdownMenuItem className=\"w-full flex-1 cursor-pointer\">\n              <LogOut className=\"mr-2 h-4 w-4\" />\n              <span>Sign out</span>\n            </DropdownMenuItem>\n          </button>\n        </form>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n}\n\nfunction Header() {\n  return (\n    <header className=\"border-b border-gray-200\">\n      <div className=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 flex justify-between items-center\">\n        <Link href=\"/\" className=\"flex items-center\">\n          <CircleIcon className=\"h-6 w-6 text-orange-500\" />\n          <span className=\"ml-2 text-xl font-semibold text-gray-900\">ACME</span>\n        </Link>\n        <div className=\"flex items-center space-x-4\">\n          <Suspense fallback={<div className=\"h-9\" />}>\n            <UserMenu />\n          </Suspense>\n        </div>\n      </div>\n    </header>\n  );\n}\n\nexport default function Layout({ children }: { children: React.ReactNode }) {\n  return (\n    <section className=\"flex flex-col min-h-screen\">\n      <Header />\n      {children}\n    </section>\n  );\n}\n"
  },
  {
    "path": "app/(dashboard)/page.tsx",
    "content": "import { Button } from '@/components/ui/button';\nimport { ArrowRight, CreditCard, Database } from 'lucide-react';\nimport { Terminal } from './terminal';\n\nexport default function HomePage() {\n  return (\n    <main>\n      <section className=\"py-20\">\n        <div className=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8\">\n          <div className=\"lg:grid lg:grid-cols-12 lg:gap-8\">\n            <div className=\"sm:text-center md:max-w-2xl md:mx-auto lg:col-span-6 lg:text-left\">\n              <h1 className=\"text-4xl font-bold text-gray-900 tracking-tight sm:text-5xl md:text-6xl\">\n                Build Your SaaS\n                <span className=\"block text-orange-500\">Faster Than Ever</span>\n              </h1>\n              <p className=\"mt-3 text-base text-gray-500 sm:mt-5 sm:text-xl lg:text-lg xl:text-xl\">\n                Launch your SaaS product in record time with our powerful,\n                ready-to-use template. Packed with modern technologies and\n                essential integrations.\n              </p>\n              <div className=\"mt-8 sm:max-w-lg sm:mx-auto sm:text-center lg:text-left lg:mx-0\">\n                <a\n                  href=\"https://vercel.com/templates/next.js/next-js-saas-starter\"\n                  target=\"_blank\"\n                >\n                  <Button\n                    size=\"lg\"\n                    variant=\"outline\"\n                    className=\"text-lg rounded-full\"\n                  >\n                    Deploy your own\n                    <ArrowRight className=\"ml-2 h-5 w-5\" />\n                  </Button>\n                </a>\n              </div>\n            </div>\n            <div className=\"mt-12 relative sm:max-w-lg sm:mx-auto lg:mt-0 lg:max-w-none lg:mx-0 lg:col-span-6 lg:flex lg:items-center\">\n              <Terminal />\n            </div>\n          </div>\n        </div>\n      </section>\n\n      <section className=\"py-16 bg-white w-full\">\n        <div className=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8\">\n          <div className=\"lg:grid lg:grid-cols-3 lg:gap-8\">\n            <div>\n              <div className=\"flex items-center justify-center h-12 w-12 rounded-md bg-orange-500 text-white\">\n                <svg viewBox=\"0 0 24 24\" className=\"h-6 w-6\">\n                  <path\n                    fill=\"currentColor\"\n                    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.38-.318-.184-.688-.277-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.44-.96-.236-2.006-.417-3.107-.534-.66-.905-1.345-1.727-2.035-2.447 1.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.442-1.107.117-2.154.298-3.113.538-.112-.49-.195-.964-.254-1.42-.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.87-.728.063-1.466.098-2.21.098-.74 0-1.477-.035-2.202-.093-.406-.582-.802-1.204-1.183-1.86-.372-.64-.71-1.29-1.018-1.946.303-.657.646-1.313 1.013-1.954.38-.66.773-1.286 1.18-1.868.728-.064 1.466-.098 2.21-.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.933-.2-.39-.41-.783-.64-1.174-.225-.392-.465-.774-.705-1.146zm3.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.493-.28-.958-.646-1.956-1.1-2.98.45-1.017.81-2.01 1.085-2.964zm-13.395.004c.278.96.645 1.957 1.1 2.98-.45 1.017-.812 2.01-1.086 2.964-.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.39.24-.375.48-.762.705-1.158.225-.39.435-.788.636-1.18zm-9.945.02c.2.392.41.783.64 1.175.23.39.465.772.705 1.143-.695-.102-1.365-.23-2.006-.386.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.295-.22-.005-.406-.05-.553-.132-.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\"\n                  />\n                </svg>\n              </div>\n              <div className=\"mt-5\">\n                <h2 className=\"text-lg font-medium text-gray-900\">\n                  Next.js and React\n                </h2>\n                <p className=\"mt-2 text-base text-gray-500\">\n                  Leverage the power of modern web technologies for optimal\n                  performance and developer experience.\n                </p>\n              </div>\n            </div>\n\n            <div className=\"mt-10 lg:mt-0\">\n              <div className=\"flex items-center justify-center h-12 w-12 rounded-md bg-orange-500 text-white\">\n                <Database className=\"h-6 w-6\" />\n              </div>\n              <div className=\"mt-5\">\n                <h2 className=\"text-lg font-medium text-gray-900\">\n                  Postgres and Drizzle ORM\n                </h2>\n                <p className=\"mt-2 text-base text-gray-500\">\n                  Robust database solution with an intuitive ORM for efficient\n                  data management and scalability.\n                </p>\n              </div>\n            </div>\n\n            <div className=\"mt-10 lg:mt-0\">\n              <div className=\"flex items-center justify-center h-12 w-12 rounded-md bg-orange-500 text-white\">\n                <CreditCard className=\"h-6 w-6\" />\n              </div>\n              <div className=\"mt-5\">\n                <h2 className=\"text-lg font-medium text-gray-900\">\n                  Stripe Integration\n                </h2>\n                <p className=\"mt-2 text-base text-gray-500\">\n                  Seamless payment processing and subscription management with\n                  industry-leading Stripe integration.\n                </p>\n              </div>\n            </div>\n          </div>\n        </div>\n      </section>\n\n      <section className=\"py-16 bg-gray-50\">\n        <div className=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8\">\n          <div className=\"lg:grid lg:grid-cols-2 lg:gap-8 lg:items-center\">\n            <div>\n              <h2 className=\"text-3xl font-bold text-gray-900 sm:text-4xl\">\n                Ready to launch your SaaS?\n              </h2>\n              <p className=\"mt-3 max-w-3xl text-lg text-gray-500\">\n                Our template provides everything you need to get your SaaS up\n                and running quickly. Don't waste time on boilerplate - focus on\n                what makes your product unique.\n              </p>\n            </div>\n            <div className=\"mt-8 lg:mt-0 flex justify-center lg:justify-end\">\n              <a href=\"https://github.com/nextjs/saas-starter\" target=\"_blank\">\n                <Button\n                  size=\"lg\"\n                  variant=\"outline\"\n                  className=\"text-lg rounded-full\"\n                >\n                  View the code\n                  <ArrowRight className=\"ml-3 h-6 w-6\" />\n                </Button>\n              </a>\n            </div>\n          </div>\n        </div>\n      </section>\n    </main>\n  );\n}\n"
  },
  {
    "path": "app/(dashboard)/pricing/page.tsx",
    "content": "import { checkoutAction } from '@/lib/payments/actions';\nimport { Check } from 'lucide-react';\nimport { getStripePrices, getStripeProducts } from '@/lib/payments/stripe';\nimport { SubmitButton } from './submit-button';\n\n// Prices are fresh for one hour max\nexport const revalidate = 3600;\n\nexport default async function PricingPage() {\n  const [prices, products] = await Promise.all([\n    getStripePrices(),\n    getStripeProducts(),\n  ]);\n\n  const basePlan = products.find((product) => product.name === 'Base');\n  const plusPlan = products.find((product) => product.name === 'Plus');\n\n  const basePrice = prices.find((price) => price.productId === basePlan?.id);\n  const plusPrice = prices.find((price) => price.productId === plusPlan?.id);\n\n  return (\n    <main className=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12\">\n      <div className=\"grid md:grid-cols-2 gap-8 max-w-xl mx-auto\">\n        <PricingCard\n          name={basePlan?.name || 'Base'}\n          price={basePrice?.unitAmount || 800}\n          interval={basePrice?.interval || 'month'}\n          trialDays={basePrice?.trialPeriodDays || 7}\n          features={[\n            'Unlimited Usage',\n            'Unlimited Workspace Members',\n            'Email Support',\n          ]}\n          priceId={basePrice?.id}\n        />\n        <PricingCard\n          name={plusPlan?.name || 'Plus'}\n          price={plusPrice?.unitAmount || 1200}\n          interval={plusPrice?.interval || 'month'}\n          trialDays={plusPrice?.trialPeriodDays || 7}\n          features={[\n            'Everything in Base, and:',\n            'Early Access to New Features',\n            '24/7 Support + Slack Access',\n          ]}\n          priceId={plusPrice?.id}\n        />\n      </div>\n    </main>\n  );\n}\n\nfunction PricingCard({\n  name,\n  price,\n  interval,\n  trialDays,\n  features,\n  priceId,\n}: {\n  name: string;\n  price: number;\n  interval: string;\n  trialDays: number;\n  features: string[];\n  priceId?: string;\n}) {\n  return (\n    <div className=\"pt-6\">\n      <h2 className=\"text-2xl font-medium text-gray-900 mb-2\">{name}</h2>\n      <p className=\"text-sm text-gray-600 mb-4\">\n        with {trialDays} day free trial\n      </p>\n      <p className=\"text-4xl font-medium text-gray-900 mb-6\">\n        ${price / 100}{' '}\n        <span className=\"text-xl font-normal text-gray-600\">\n          per user / {interval}\n        </span>\n      </p>\n      <ul className=\"space-y-4 mb-8\">\n        {features.map((feature, index) => (\n          <li key={index} className=\"flex items-start\">\n            <Check className=\"h-5 w-5 text-orange-500 mr-2 mt-0.5 flex-shrink-0\" />\n            <span className=\"text-gray-700\">{feature}</span>\n          </li>\n        ))}\n      </ul>\n      <form action={checkoutAction}>\n        <input type=\"hidden\" name=\"priceId\" value={priceId} />\n        <SubmitButton />\n      </form>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/(dashboard)/pricing/submit-button.tsx",
    "content": "'use client';\n\nimport { Button } from '@/components/ui/button';\nimport { ArrowRight, Loader2 } from 'lucide-react';\nimport { useFormStatus } from 'react-dom';\n\nexport function SubmitButton() {\n  const { pending } = useFormStatus();\n\n  return (\n    <Button\n      type=\"submit\"\n      disabled={pending}\n      variant=\"outline\"\n      className=\"w-full rounded-full\"\n    >\n      {pending ? (\n        <>\n          <Loader2 className=\"animate-spin mr-2 h-4 w-4\" />\n          Loading...\n        </>\n      ) : (\n        <>\n          Get Started\n          <ArrowRight className=\"ml-2 h-4 w-4\" />\n        </>\n      )}\n    </Button>\n  );\n}\n"
  },
  {
    "path": "app/(dashboard)/terminal.tsx",
    "content": "'use client';\n\nimport { useState, useEffect } from 'react';\nimport { Copy, Check } from 'lucide-react';\n\nexport function Terminal() {\n  const [terminalStep, setTerminalStep] = useState(0);\n  const [copied, setCopied] = useState(false);\n  const terminalSteps = [\n    'git clone https://github.com/nextjs/saas-starter',\n    'pnpm install',\n    'pnpm db:setup',\n    'pnpm db:migrate',\n    'pnpm db:seed',\n    'pnpm dev 🎉',\n  ];\n\n  useEffect(() => {\n    const timer = setTimeout(() => {\n      setTerminalStep((prev) =>\n        prev < terminalSteps.length - 1 ? prev + 1 : prev\n      );\n    }, 500);\n\n    return () => clearTimeout(timer);\n  }, [terminalStep]);\n\n  const copyToClipboard = () => {\n    navigator.clipboard.writeText(terminalSteps.join('\\n'));\n    setCopied(true);\n    setTimeout(() => setCopied(false), 2000);\n  };\n\n  return (\n    <div className=\"w-full rounded-lg shadow-lg overflow-hidden bg-gray-900 text-white font-mono text-sm relative\">\n      <div className=\"p-4\">\n        <div className=\"flex justify-between items-center mb-4\">\n          <div className=\"flex space-x-2\">\n            <div className=\"w-3 h-3 rounded-full bg-red-500\"></div>\n            <div className=\"w-3 h-3 rounded-full bg-yellow-500\"></div>\n            <div className=\"w-3 h-3 rounded-full bg-green-500\"></div>\n          </div>\n          <button\n            onClick={copyToClipboard}\n            className=\"text-gray-400 hover:text-white transition-colors\"\n            aria-label=\"Copy to clipboard\"\n          >\n            {copied ? (\n              <Check className=\"h-5 w-5\" />\n            ) : (\n              <Copy className=\"h-5 w-5\" />\n            )}\n          </button>\n        </div>\n        <div className=\"space-y-2\">\n          {terminalSteps.map((step, index) => (\n            <div\n              key={index}\n              className={`${index > terminalStep ? 'opacity-0' : 'opacity-100'} transition-opacity duration-300`}\n            >\n              <span className=\"text-green-400\">$</span> {step}\n            </div>\n          ))}\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/(login)/actions.ts",
    "content": "'use server';\n\nimport { z } from 'zod';\nimport { and, eq, sql } from 'drizzle-orm';\nimport { db } from '@/lib/db/drizzle';\nimport {\n  User,\n  users,\n  teams,\n  teamMembers,\n  activityLogs,\n  type NewUser,\n  type NewTeam,\n  type NewTeamMember,\n  type NewActivityLog,\n  ActivityType,\n  invitations\n} from '@/lib/db/schema';\nimport { comparePasswords, hashPassword, setSession } from '@/lib/auth/session';\nimport { redirect } from 'next/navigation';\nimport { cookies } from 'next/headers';\nimport { createCheckoutSession } from '@/lib/payments/stripe';\nimport { getUser, getUserWithTeam } from '@/lib/db/queries';\nimport {\n  validatedAction,\n  validatedActionWithUser\n} from '@/lib/auth/middleware';\n\nasync function logActivity(\n  teamId: number | null | undefined,\n  userId: number,\n  type: ActivityType,\n  ipAddress?: string\n) {\n  if (teamId === null || teamId === undefined) {\n    return;\n  }\n  const newActivity: NewActivityLog = {\n    teamId,\n    userId,\n    action: type,\n    ipAddress: ipAddress || ''\n  };\n  await db.insert(activityLogs).values(newActivity);\n}\n\nconst signInSchema = z.object({\n  email: z.string().email().min(3).max(255),\n  password: z.string().min(8).max(100)\n});\n\nexport const signIn = validatedAction(signInSchema, async (data, formData) => {\n  const { email, password } = data;\n\n  const userWithTeam = await db\n    .select({\n      user: users,\n      team: teams\n    })\n    .from(users)\n    .leftJoin(teamMembers, eq(users.id, teamMembers.userId))\n    .leftJoin(teams, eq(teamMembers.teamId, teams.id))\n    .where(eq(users.email, email))\n    .limit(1);\n\n  if (userWithTeam.length === 0) {\n    return {\n      error: 'Invalid email or password. Please try again.',\n      email,\n      password\n    };\n  }\n\n  const { user: foundUser, team: foundTeam } = userWithTeam[0];\n\n  const isPasswordValid = await comparePasswords(\n    password,\n    foundUser.passwordHash\n  );\n\n  if (!isPasswordValid) {\n    return {\n      error: 'Invalid email or password. Please try again.',\n      email,\n      password\n    };\n  }\n\n  await Promise.all([\n    setSession(foundUser),\n    logActivity(foundTeam?.id, foundUser.id, ActivityType.SIGN_IN)\n  ]);\n\n  const redirectTo = formData.get('redirect') as string | null;\n  if (redirectTo === 'checkout') {\n    const priceId = formData.get('priceId') as string;\n    return createCheckoutSession({ team: foundTeam, priceId });\n  }\n\n  redirect('/dashboard');\n});\n\nconst signUpSchema = z.object({\n  email: z.string().email(),\n  password: z.string().min(8),\n  inviteId: z.string().optional()\n});\n\nexport const signUp = validatedAction(signUpSchema, async (data, formData) => {\n  const { email, password, inviteId } = data;\n\n  const existingUser = await db\n    .select()\n    .from(users)\n    .where(eq(users.email, email))\n    .limit(1);\n\n  if (existingUser.length > 0) {\n    return {\n      error: 'Failed to create user. Please try again.',\n      email,\n      password\n    };\n  }\n\n  const passwordHash = await hashPassword(password);\n\n  const newUser: NewUser = {\n    email,\n    passwordHash,\n    role: 'owner' // Default role, will be overridden if there's an invitation\n  };\n\n  const [createdUser] = await db.insert(users).values(newUser).returning();\n\n  if (!createdUser) {\n    return {\n      error: 'Failed to create user. Please try again.',\n      email,\n      password\n    };\n  }\n\n  let teamId: number;\n  let userRole: string;\n  let createdTeam: typeof teams.$inferSelect | null = null;\n\n  if (inviteId) {\n    // Check if there's a valid invitation\n    const [invitation] = await db\n      .select()\n      .from(invitations)\n      .where(\n        and(\n          eq(invitations.id, parseInt(inviteId)),\n          eq(invitations.email, email),\n          eq(invitations.status, 'pending')\n        )\n      )\n      .limit(1);\n\n    if (invitation) {\n      teamId = invitation.teamId;\n      userRole = invitation.role;\n\n      await db\n        .update(invitations)\n        .set({ status: 'accepted' })\n        .where(eq(invitations.id, invitation.id));\n\n      await logActivity(teamId, createdUser.id, ActivityType.ACCEPT_INVITATION);\n\n      [createdTeam] = await db\n        .select()\n        .from(teams)\n        .where(eq(teams.id, teamId))\n        .limit(1);\n    } else {\n      return { error: 'Invalid or expired invitation.', email, password };\n    }\n  } else {\n    // Create a new team if there's no invitation\n    const newTeam: NewTeam = {\n      name: `${email}'s Team`\n    };\n\n    [createdTeam] = await db.insert(teams).values(newTeam).returning();\n\n    if (!createdTeam) {\n      return {\n        error: 'Failed to create team. Please try again.',\n        email,\n        password\n      };\n    }\n\n    teamId = createdTeam.id;\n    userRole = 'owner';\n\n    await logActivity(teamId, createdUser.id, ActivityType.CREATE_TEAM);\n  }\n\n  const newTeamMember: NewTeamMember = {\n    userId: createdUser.id,\n    teamId: teamId,\n    role: userRole\n  };\n\n  await Promise.all([\n    db.insert(teamMembers).values(newTeamMember),\n    logActivity(teamId, createdUser.id, ActivityType.SIGN_UP),\n    setSession(createdUser)\n  ]);\n\n  const redirectTo = formData.get('redirect') as string | null;\n  if (redirectTo === 'checkout') {\n    const priceId = formData.get('priceId') as string;\n    return createCheckoutSession({ team: createdTeam, priceId });\n  }\n\n  redirect('/dashboard');\n});\n\nexport async function signOut() {\n  const user = (await getUser()) as User;\n  const userWithTeam = await getUserWithTeam(user.id);\n  await logActivity(userWithTeam?.teamId, user.id, ActivityType.SIGN_OUT);\n  (await cookies()).delete('session');\n}\n\nconst updatePasswordSchema = z.object({\n  currentPassword: z.string().min(8).max(100),\n  newPassword: z.string().min(8).max(100),\n  confirmPassword: z.string().min(8).max(100)\n});\n\nexport const updatePassword = validatedActionWithUser(\n  updatePasswordSchema,\n  async (data, _, user) => {\n    const { currentPassword, newPassword, confirmPassword } = data;\n\n    const isPasswordValid = await comparePasswords(\n      currentPassword,\n      user.passwordHash\n    );\n\n    if (!isPasswordValid) {\n      return {\n        currentPassword,\n        newPassword,\n        confirmPassword,\n        error: 'Current password is incorrect.'\n      };\n    }\n\n    if (currentPassword === newPassword) {\n      return {\n        currentPassword,\n        newPassword,\n        confirmPassword,\n        error: 'New password must be different from the current password.'\n      };\n    }\n\n    if (confirmPassword !== newPassword) {\n      return {\n        currentPassword,\n        newPassword,\n        confirmPassword,\n        error: 'New password and confirmation password do not match.'\n      };\n    }\n\n    const newPasswordHash = await hashPassword(newPassword);\n    const userWithTeam = await getUserWithTeam(user.id);\n\n    await Promise.all([\n      db\n        .update(users)\n        .set({ passwordHash: newPasswordHash })\n        .where(eq(users.id, user.id)),\n      logActivity(userWithTeam?.teamId, user.id, ActivityType.UPDATE_PASSWORD)\n    ]);\n\n    return {\n      success: 'Password updated successfully.'\n    };\n  }\n);\n\nconst deleteAccountSchema = z.object({\n  password: z.string().min(8).max(100)\n});\n\nexport const deleteAccount = validatedActionWithUser(\n  deleteAccountSchema,\n  async (data, _, user) => {\n    const { password } = data;\n\n    const isPasswordValid = await comparePasswords(password, user.passwordHash);\n    if (!isPasswordValid) {\n      return {\n        password,\n        error: 'Incorrect password. Account deletion failed.'\n      };\n    }\n\n    const userWithTeam = await getUserWithTeam(user.id);\n\n    await logActivity(\n      userWithTeam?.teamId,\n      user.id,\n      ActivityType.DELETE_ACCOUNT\n    );\n\n    // Soft delete\n    await db\n      .update(users)\n      .set({\n        deletedAt: sql`CURRENT_TIMESTAMP`,\n        email: sql`CONCAT(email, '-', id, '-deleted')` // Ensure email uniqueness\n      })\n      .where(eq(users.id, user.id));\n\n    if (userWithTeam?.teamId) {\n      await db\n        .delete(teamMembers)\n        .where(\n          and(\n            eq(teamMembers.userId, user.id),\n            eq(teamMembers.teamId, userWithTeam.teamId)\n          )\n        );\n    }\n\n    (await cookies()).delete('session');\n    redirect('/sign-in');\n  }\n);\n\nconst updateAccountSchema = z.object({\n  name: z.string().min(1, 'Name is required').max(100),\n  email: z.string().email('Invalid email address')\n});\n\nexport const updateAccount = validatedActionWithUser(\n  updateAccountSchema,\n  async (data, _, user) => {\n    const { name, email } = data;\n    const userWithTeam = await getUserWithTeam(user.id);\n\n    await Promise.all([\n      db.update(users).set({ name, email }).where(eq(users.id, user.id)),\n      logActivity(userWithTeam?.teamId, user.id, ActivityType.UPDATE_ACCOUNT)\n    ]);\n\n    return { name, success: 'Account updated successfully.' };\n  }\n);\n\nconst removeTeamMemberSchema = z.object({\n  memberId: z.number()\n});\n\nexport const removeTeamMember = validatedActionWithUser(\n  removeTeamMemberSchema,\n  async (data, _, user) => {\n    const { memberId } = data;\n    const userWithTeam = await getUserWithTeam(user.id);\n\n    if (!userWithTeam?.teamId) {\n      return { error: 'User is not part of a team' };\n    }\n\n    await db\n      .delete(teamMembers)\n      .where(\n        and(\n          eq(teamMembers.id, memberId),\n          eq(teamMembers.teamId, userWithTeam.teamId)\n        )\n      );\n\n    await logActivity(\n      userWithTeam.teamId,\n      user.id,\n      ActivityType.REMOVE_TEAM_MEMBER\n    );\n\n    return { success: 'Team member removed successfully' };\n  }\n);\n\nconst inviteTeamMemberSchema = z.object({\n  email: z.string().email('Invalid email address'),\n  role: z.enum(['member', 'owner'])\n});\n\nexport const inviteTeamMember = validatedActionWithUser(\n  inviteTeamMemberSchema,\n  async (data, _, user) => {\n    const { email, role } = data;\n    const userWithTeam = await getUserWithTeam(user.id);\n\n    if (!userWithTeam?.teamId) {\n      return { error: 'User is not part of a team' };\n    }\n\n    const existingMember = await db\n      .select()\n      .from(users)\n      .leftJoin(teamMembers, eq(users.id, teamMembers.userId))\n      .where(\n        and(eq(users.email, email), eq(teamMembers.teamId, userWithTeam.teamId))\n      )\n      .limit(1);\n\n    if (existingMember.length > 0) {\n      return { error: 'User is already a member of this team' };\n    }\n\n    // Check if there's an existing invitation\n    const existingInvitation = await db\n      .select()\n      .from(invitations)\n      .where(\n        and(\n          eq(invitations.email, email),\n          eq(invitations.teamId, userWithTeam.teamId),\n          eq(invitations.status, 'pending')\n        )\n      )\n      .limit(1);\n\n    if (existingInvitation.length > 0) {\n      return { error: 'An invitation has already been sent to this email' };\n    }\n\n    // Create a new invitation\n    await db.insert(invitations).values({\n      teamId: userWithTeam.teamId,\n      email,\n      role,\n      invitedBy: user.id,\n      status: 'pending'\n    });\n\n    await logActivity(\n      userWithTeam.teamId,\n      user.id,\n      ActivityType.INVITE_TEAM_MEMBER\n    );\n\n    // TODO: Send invitation email and include ?inviteId={id} to sign-up URL\n    // await sendInvitationEmail(email, userWithTeam.team.name, role)\n\n    return { success: 'Invitation sent successfully' };\n  }\n);\n"
  },
  {
    "path": "app/(login)/login.tsx",
    "content": "'use client';\n\nimport Link from 'next/link';\nimport { useActionState } from 'react';\nimport { useSearchParams } from 'next/navigation';\nimport { Button } from '@/components/ui/button';\nimport { Input } from '@/components/ui/input';\nimport { Label } from '@/components/ui/label';\nimport { CircleIcon, Loader2 } from 'lucide-react';\nimport { signIn, signUp } from './actions';\nimport { ActionState } from '@/lib/auth/middleware';\n\nexport function Login({ mode = 'signin' }: { mode?: 'signin' | 'signup' }) {\n  const searchParams = useSearchParams();\n  const redirect = searchParams.get('redirect');\n  const priceId = searchParams.get('priceId');\n  const inviteId = searchParams.get('inviteId');\n  const [state, formAction, pending] = useActionState<ActionState, FormData>(\n    mode === 'signin' ? signIn : signUp,\n    { error: '' }\n  );\n\n  return (\n    <div className=\"min-h-[100dvh] flex flex-col justify-center py-12 px-4 sm:px-6 lg:px-8 bg-gray-50\">\n      <div className=\"sm:mx-auto sm:w-full sm:max-w-md\">\n        <div className=\"flex justify-center\">\n          <CircleIcon className=\"h-12 w-12 text-orange-500\" />\n        </div>\n        <h2 className=\"mt-6 text-center text-3xl font-extrabold text-gray-900\">\n          {mode === 'signin'\n            ? 'Sign in to your account'\n            : 'Create your account'}\n        </h2>\n      </div>\n\n      <div className=\"mt-8 sm:mx-auto sm:w-full sm:max-w-md\">\n        <form className=\"space-y-6\" action={formAction}>\n          <input type=\"hidden\" name=\"redirect\" value={redirect || ''} />\n          <input type=\"hidden\" name=\"priceId\" value={priceId || ''} />\n          <input type=\"hidden\" name=\"inviteId\" value={inviteId || ''} />\n          <div>\n            <Label\n              htmlFor=\"email\"\n              className=\"block text-sm font-medium text-gray-700\"\n            >\n              Email\n            </Label>\n            <div className=\"mt-1\">\n              <Input\n                id=\"email\"\n                name=\"email\"\n                type=\"email\"\n                autoComplete=\"email\"\n                defaultValue={state.email}\n                required\n                maxLength={50}\n                className=\"appearance-none rounded-full relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-orange-500 focus:border-orange-500 focus:z-10 sm:text-sm\"\n                placeholder=\"Enter your email\"\n              />\n            </div>\n          </div>\n\n          <div>\n            <Label\n              htmlFor=\"password\"\n              className=\"block text-sm font-medium text-gray-700\"\n            >\n              Password\n            </Label>\n            <div className=\"mt-1\">\n              <Input\n                id=\"password\"\n                name=\"password\"\n                type=\"password\"\n                autoComplete={\n                  mode === 'signin' ? 'current-password' : 'new-password'\n                }\n                defaultValue={state.password}\n                required\n                minLength={8}\n                maxLength={100}\n                className=\"appearance-none rounded-full relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-orange-500 focus:border-orange-500 focus:z-10 sm:text-sm\"\n                placeholder=\"Enter your password\"\n              />\n            </div>\n          </div>\n\n          {state?.error && (\n            <div className=\"text-red-500 text-sm\">{state.error}</div>\n          )}\n\n          <div>\n            <Button\n              type=\"submit\"\n              className=\"w-full flex justify-center items-center py-2 px-4 border border-transparent rounded-full shadow-sm text-sm font-medium text-white bg-orange-600 hover:bg-orange-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-orange-500\"\n              disabled={pending}\n            >\n              {pending ? (\n                <>\n                  <Loader2 className=\"animate-spin mr-2 h-4 w-4\" />\n                  Loading...\n                </>\n              ) : mode === 'signin' ? (\n                'Sign in'\n              ) : (\n                'Sign up'\n              )}\n            </Button>\n          </div>\n        </form>\n\n        <div className=\"mt-6\">\n          <div className=\"relative\">\n            <div className=\"absolute inset-0 flex items-center\">\n              <div className=\"w-full border-t border-gray-300\" />\n            </div>\n            <div className=\"relative flex justify-center text-sm\">\n              <span className=\"px-2 bg-gray-50 text-gray-500\">\n                {mode === 'signin'\n                  ? 'New to our platform?'\n                  : 'Already have an account?'}\n              </span>\n            </div>\n          </div>\n\n          <div className=\"mt-6\">\n            <Link\n              href={`${mode === 'signin' ? '/sign-up' : '/sign-in'}${\n                redirect ? `?redirect=${redirect}` : ''\n              }${priceId ? `&priceId=${priceId}` : ''}`}\n              className=\"w-full flex justify-center py-2 px-4 border border-gray-300 rounded-full shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-orange-500\"\n            >\n              {mode === 'signin'\n                ? 'Create an account'\n                : 'Sign in to existing account'}\n            </Link>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/(login)/sign-in/page.tsx",
    "content": "import { Suspense } from 'react';\nimport { Login } from '../login';\n\nexport default function SignInPage() {\n  return (\n    <Suspense>\n      <Login mode=\"signin\" />\n    </Suspense>\n  );\n}\n"
  },
  {
    "path": "app/(login)/sign-up/page.tsx",
    "content": "import { Suspense } from 'react';\nimport { Login } from '../login';\n\nexport default function SignUpPage() {\n  return (\n    <Suspense>\n      <Login mode=\"signup\" />\n    </Suspense>\n  );\n}\n"
  },
  {
    "path": "app/api/stripe/checkout/route.ts",
    "content": "import { eq } from 'drizzle-orm';\nimport { db } from '@/lib/db/drizzle';\nimport { users, teams, teamMembers } from '@/lib/db/schema';\nimport { setSession } from '@/lib/auth/session';\nimport { NextRequest, NextResponse } from 'next/server';\nimport { stripe } from '@/lib/payments/stripe';\nimport Stripe from 'stripe';\n\nexport async function GET(request: NextRequest) {\n  const searchParams = request.nextUrl.searchParams;\n  const sessionId = searchParams.get('session_id');\n\n  if (!sessionId) {\n    return NextResponse.redirect(new URL('/pricing', request.url));\n  }\n\n  try {\n    const session = await stripe.checkout.sessions.retrieve(sessionId, {\n      expand: ['customer', 'subscription'],\n    });\n\n    if (!session.customer || typeof session.customer === 'string') {\n      throw new Error('Invalid customer data from Stripe.');\n    }\n\n    const customerId = session.customer.id;\n    const subscriptionId =\n      typeof session.subscription === 'string'\n        ? session.subscription\n        : session.subscription?.id;\n\n    if (!subscriptionId) {\n      throw new Error('No subscription found for this session.');\n    }\n\n    const subscription = await stripe.subscriptions.retrieve(subscriptionId, {\n      expand: ['items.data.price.product'],\n    });\n\n    const plan = subscription.items.data[0]?.price;\n\n    if (!plan) {\n      throw new Error('No plan found for this subscription.');\n    }\n\n    const productId = (plan.product as Stripe.Product).id;\n\n    if (!productId) {\n      throw new Error('No product ID found for this subscription.');\n    }\n\n    const userId = session.client_reference_id;\n    if (!userId) {\n      throw new Error(\"No user ID found in session's client_reference_id.\");\n    }\n\n    const user = await db\n      .select()\n      .from(users)\n      .where(eq(users.id, Number(userId)))\n      .limit(1);\n\n    if (user.length === 0) {\n      throw new Error('User not found in database.');\n    }\n\n    const userTeam = await db\n      .select({\n        teamId: teamMembers.teamId,\n      })\n      .from(teamMembers)\n      .where(eq(teamMembers.userId, user[0].id))\n      .limit(1);\n\n    if (userTeam.length === 0) {\n      throw new Error('User is not associated with any team.');\n    }\n\n    await db\n      .update(teams)\n      .set({\n        stripeCustomerId: customerId,\n        stripeSubscriptionId: subscriptionId,\n        stripeProductId: productId,\n        planName: (plan.product as Stripe.Product).name,\n        subscriptionStatus: subscription.status,\n        updatedAt: new Date(),\n      })\n      .where(eq(teams.id, userTeam[0].teamId));\n\n    await setSession(user[0]);\n    return NextResponse.redirect(new URL('/dashboard', request.url));\n  } catch (error) {\n    console.error('Error handling successful checkout:', error);\n    return NextResponse.redirect(new URL('/error', request.url));\n  }\n}\n"
  },
  {
    "path": "app/api/stripe/webhook/route.ts",
    "content": "import Stripe from 'stripe';\nimport { handleSubscriptionChange, stripe } from '@/lib/payments/stripe';\nimport { NextRequest, NextResponse } from 'next/server';\n\nconst webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;\n\nexport async function POST(request: NextRequest) {\n  const payload = await request.text();\n  const signature = request.headers.get('stripe-signature') as string;\n\n  let event: Stripe.Event;\n\n  try {\n    event = stripe.webhooks.constructEvent(payload, signature, webhookSecret);\n  } catch (err) {\n    console.error('Webhook signature verification failed.', err);\n    return NextResponse.json(\n      { error: 'Webhook signature verification failed.' },\n      { status: 400 }\n    );\n  }\n\n  switch (event.type) {\n    case 'customer.subscription.updated':\n    case 'customer.subscription.deleted':\n      const subscription = event.data.object as Stripe.Subscription;\n      await handleSubscriptionChange(subscription);\n      break;\n    default:\n      console.log(`Unhandled event type ${event.type}`);\n  }\n\n  return NextResponse.json({ received: true });\n}\n"
  },
  {
    "path": "app/api/team/route.ts",
    "content": "import { getTeamForUser } from '@/lib/db/queries';\n\nexport async function GET() {\n  const team = await getTeamForUser();\n  return Response.json(team);\n}\n"
  },
  {
    "path": "app/api/user/route.ts",
    "content": "import { getUser } from '@/lib/db/queries';\n\nexport async function GET() {\n  const user = await getUser();\n  return Response.json(user);\n}\n"
  },
  {
    "path": "app/globals.css",
    "content": "@import \"tailwindcss\";\n/*\n  ---break---\n*/\n@custom-variant dark (&:is(.dark *));\n\n@import \"tw-animate-css\";\n\n@variant dark (&:is(.dark *));\n\n@theme {\n  --color-background: hsl(var(--background));\n  --color-foreground: hsl(var(--foreground));\n\n  --color-card: hsl(var(--card));\n  --color-card-foreground: hsl(var(--card-foreground));\n\n  --color-popover: hsl(var(--popover));\n  --color-popover-foreground: hsl(var(--popover-foreground));\n\n  --color-primary: hsl(var(--primary));\n  --color-primary-foreground: hsl(var(--primary-foreground));\n\n  --color-secondary: hsl(var(--secondary));\n  --color-secondary-foreground: hsl(var(--secondary-foreground));\n\n  --color-muted: hsl(var(--muted));\n  --color-muted-foreground: hsl(var(--muted-foreground));\n\n  --color-accent: hsl(var(--accent));\n  --color-accent-foreground: hsl(var(--accent-foreground));\n\n  --color-destructive: hsl(var(--destructive));\n  --color-destructive-foreground: hsl(var(--destructive-foreground));\n\n  --color-border: hsl(var(--border));\n  --color-input: hsl(var(--input));\n  --color-ring: hsl(var(--ring));\n\n  --color-chart-1: hsl(var(--chart-1));\n  --color-chart-2: hsl(var(--chart-2));\n  --color-chart-3: hsl(var(--chart-3));\n  --color-chart-4: hsl(var(--chart-4));\n  --color-chart-5: hsl(var(--chart-5));\n\n  --color-sidebar: hsl(var(--sidebar-background));\n  --color-sidebar-foreground: hsl(var(--sidebar-foreground));\n  --color-sidebar-primary: hsl(var(--sidebar-primary));\n  --color-sidebar-primary-foreground: hsl(var(--sidebar-primary-foreground));\n  --color-sidebar-accent: hsl(var(--sidebar-accent));\n  --color-sidebar-accent-foreground: hsl(var(--sidebar-accent-foreground));\n  --color-sidebar-border: hsl(var(--sidebar-border));\n  --color-sidebar-ring: hsl(var(--sidebar-ring));\n\n  --radius-lg: var(--radius);\n  --radius-md: calc(var(--radius) - 2px);\n  --radius-sm: calc(var(--radius) - 4px);\n}\n\n/*\n  The default border color has changed to `currentColor` in Tailwind CSS v4,\n  so we've added these compatibility styles to make sure everything still\n  looks the same as it did with Tailwind CSS v3.\n\n  If we ever want to remove these styles, we need to add an explicit border\n  color utility to any element that depends on these defaults.\n*/\n@layer base {\n  *,\n  ::after,\n  ::before,\n  ::backdrop,\n  ::file-selector-button {\n    border-color: var(--color-gray-200, currentColor);\n  }\n}\n\n@layer utilities {\n  body {\n    font-family: \"Manrope\", Arial, Helvetica, sans-serif;\n  }\n}\n\n@layer base {\n  :root {\n    --background: 0 0% 100%;\n    --foreground: 222.2 84% 4.9%;\n    --card: 0 0% 100%;\n    --card-foreground: 222.2 84% 4.9%;\n    --popover: 0 0% 100%;\n    --popover-foreground: 222.2 84% 4.9%;\n    --primary: 222.2 47.4% 11.2%;\n    --primary-foreground: 210 40% 98%;\n    --secondary: 210 40% 96.1%;\n    --secondary-foreground: 222.2 47.4% 11.2%;\n    --muted: 210 40% 96.1%;\n    --muted-foreground: 215.4 16.3% 46.9%;\n    --accent: 210 40% 96.1%;\n    --accent-foreground: 222.2 47.4% 11.2%;\n    --destructive: 0 84.2% 60.2%;\n    --destructive-foreground: 210 40% 98%;\n    --border: 214.3 31.8% 91.4%;\n    --input: 214.3 31.8% 91.4%;\n    --ring: 222.2 84% 4.9%;\n    --chart-1: 12 76% 61%;\n    --chart-2: 173 58% 39%;\n    --chart-3: 197 37% 24%;\n    --chart-4: 43 74% 66%;\n    --chart-5: 27 87% 67%;\n    --radius: 0.5rem;\n    --sidebar-background: 0 0% 98%;\n    --sidebar-foreground: 240 5.3% 26.1%;\n    --sidebar-primary: 240 5.9% 10%;\n    --sidebar-primary-foreground: 0 0% 98%;\n    --sidebar-accent: 240 4.8% 95.9%;\n    --sidebar-accent-foreground: 240 5.9% 10%;\n    --sidebar-border: 220 13% 91%;\n    --sidebar-ring: 217.2 91.2% 59.8%;\n  }\n\n  .dark {\n    --background: 222.2 84% 4.9%;\n    --foreground: 210 40% 98%;\n    --card: 222.2 84% 4.9%;\n    --card-foreground: 210 40% 98%;\n    --popover: 222.2 84% 4.9%;\n    --popover-foreground: 210 40% 98%;\n    --primary: 210 40% 98%;\n    --primary-foreground: 222.2 47.4% 11.2%;\n    --secondary: 217.2 32.6% 17.5%;\n    --secondary-foreground: 210 40% 98%;\n    --muted: 217.2 32.6% 17.5%;\n    --muted-foreground: 215 20.2% 65.1%;\n    --accent: 217.2 32.6% 17.5%;\n    --accent-foreground: 210 40% 98%;\n    --destructive: 0 62.8% 30.6%;\n    --destructive-foreground: 210 40% 98%;\n    --border: 217.2 32.6% 17.5%;\n    --input: 217.2 32.6% 17.5%;\n    --ring: 212.7 26.8% 83.9%;\n    --chart-1: 220 70% 50%;\n    --chart-2: 160 60% 45%;\n    --chart-3: 30 80% 55%;\n    --chart-4: 280 65% 60%;\n    --chart-5: 340 75% 55%;\n    --sidebar-background: 240 5.9% 10%;\n    --sidebar-foreground: 240 4.8% 95.9%;\n    --sidebar-primary: 224.3 76.3% 48%;\n    --sidebar-primary-foreground: 0 0% 100%;\n    --sidebar-accent: 240 3.7% 15.9%;\n    --sidebar-accent-foreground: 240 4.8% 95.9%;\n    --sidebar-border: 240 3.7% 15.9%;\n    --sidebar-ring: 217.2 91.2% 59.8%;\n  }\n}\n\n@layer base {\n  * {\n    @apply border-border;\n  }\n  body {\n    @apply bg-background text-foreground;\n  }\n}\n\n/*\n  ---break---\n*/\n\n:root {\n  --background: hsl(0 0% 100%);\n  --foreground: hsl(240 10% 3.9%);\n  --card: hsl(0 0% 100%);\n  --card-foreground: hsl(240 10% 3.9%);\n  --popover: hsl(0 0% 100%);\n  --popover-foreground: hsl(240 10% 3.9%);\n  --primary: hsl(240 5.9% 10%);\n  --primary-foreground: hsl(0 0% 98%);\n  --secondary: hsl(240 4.8% 95.9%);\n  --secondary-foreground: hsl(240 5.9% 10%);\n  --muted: hsl(240 4.8% 95.9%);\n  --muted-foreground: hsl(240 3.8% 46.1%);\n  --accent: hsl(240 4.8% 95.9%);\n  --accent-foreground: hsl(240 5.9% 10%);\n  --destructive: hsl(0 84.2% 60.2%);\n  --destructive-foreground: hsl(0 0% 98%);\n  --border: hsl(240 5.9% 90%);\n  --input: hsl(240 5.9% 90%);\n  --ring: hsl(240 10% 3.9%);\n  --chart-1: hsl(12 76% 61%);\n  --chart-2: hsl(173 58% 39%);\n  --chart-3: hsl(197 37% 24%);\n  --chart-4: hsl(43 74% 66%);\n  --chart-5: hsl(27 87% 67%);\n  --radius: 0.6rem;\n}\n\n/*\n  ---break---\n*/\n\n.dark {\n  --background: hsl(240 10% 3.9%);\n  --foreground: hsl(0 0% 98%);\n  --card: hsl(240 10% 3.9%);\n  --card-foreground: hsl(0 0% 98%);\n  --popover: hsl(240 10% 3.9%);\n  --popover-foreground: hsl(0 0% 98%);\n  --primary: hsl(0 0% 98%);\n  --primary-foreground: hsl(240 5.9% 10%);\n  --secondary: hsl(240 3.7% 15.9%);\n  --secondary-foreground: hsl(0 0% 98%);\n  --muted: hsl(240 3.7% 15.9%);\n  --muted-foreground: hsl(240 5% 64.9%);\n  --accent: hsl(240 3.7% 15.9%);\n  --accent-foreground: hsl(0 0% 98%);\n  --destructive: hsl(0 62.8% 30.6%);\n  --destructive-foreground: hsl(0 0% 98%);\n  --border: hsl(240 3.7% 15.9%);\n  --input: hsl(240 3.7% 15.9%);\n  --ring: hsl(240 4.9% 83.9%);\n  --chart-1: hsl(220 70% 50%);\n  --chart-2: hsl(160 60% 45%);\n  --chart-3: hsl(30 80% 55%);\n  --chart-4: hsl(280 65% 60%);\n  --chart-5: hsl(340 75% 55%);\n}\n\n/*\n  ---break---\n*/\n\n@theme inline {\n  --color-background: var(--background);\n  --color-foreground: var(--foreground);\n  --color-card: var(--card);\n  --color-card-foreground: var(--card-foreground);\n  --color-popover: var(--popover);\n  --color-popover-foreground: var(--popover-foreground);\n  --color-primary: var(--primary);\n  --color-primary-foreground: var(--primary-foreground);\n  --color-secondary: var(--secondary);\n  --color-secondary-foreground: var(--secondary-foreground);\n  --color-muted: var(--muted);\n  --color-muted-foreground: var(--muted-foreground);\n  --color-accent: var(--accent);\n  --color-accent-foreground: var(--accent-foreground);\n  --color-destructive: var(--destructive);\n  --color-destructive-foreground: var(--destructive-foreground);\n  --color-border: var(--border);\n  --color-input: var(--input);\n  --color-ring: var(--ring);\n  --color-chart-1: var(--chart-1);\n  --color-chart-2: var(--chart-2);\n  --color-chart-3: var(--chart-3);\n  --color-chart-4: var(--chart-4);\n  --color-chart-5: var(--chart-5);\n  --radius-sm: calc(var(--radius) - 4px);\n  --radius-md: calc(var(--radius) - 2px);\n  --radius-lg: var(--radius);\n  --radius-xl: calc(var(--radius) + 4px);\n}\n\n/*\n  ---break---\n*/\n\n@layer base {\n  * {\n    @apply border-border outline-ring/50;\n  }\n  body {\n    @apply bg-background text-foreground;\n  }\n}\n"
  },
  {
    "path": "app/layout.tsx",
    "content": "import './globals.css';\nimport type { Metadata, Viewport } from 'next';\nimport { Manrope } from 'next/font/google';\nimport { getUser, getTeamForUser } from '@/lib/db/queries';\nimport { SWRConfig } from 'swr';\n\nexport const metadata: Metadata = {\n  title: 'Next.js SaaS Starter',\n  description: 'Get started quickly with Next.js, Postgres, and Stripe.'\n};\n\nexport const viewport: Viewport = {\n  maximumScale: 1\n};\n\nconst manrope = Manrope({ subsets: ['latin'] });\n\nexport default function RootLayout({\n  children\n}: {\n  children: React.ReactNode;\n}) {\n  return (\n    <html\n      lang=\"en\"\n      className={`bg-white dark:bg-gray-950 text-black dark:text-white ${manrope.className}`}\n    >\n      <body className=\"min-h-[100dvh] bg-gray-50\">\n        <SWRConfig\n          value={{\n            fallback: {\n              // We do NOT await here\n              // Only components that read this data will suspend\n              '/api/user': getUser(),\n              '/api/team': getTeamForUser()\n            }\n          }}\n        >\n          {children}\n        </SWRConfig>\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "app/not-found.tsx",
    "content": "import Link from 'next/link';\nimport { CircleIcon } from 'lucide-react';\n\nexport default function NotFound() {\n  return (\n    <div className=\"flex items-center justify-center min-h-[100dvh]\">\n      <div className=\"max-w-md space-y-8 p-4 text-center\">\n        <div className=\"flex justify-center\">\n          <CircleIcon className=\"size-12 text-orange-500\" />\n        </div>\n        <h1 className=\"text-4xl font-bold text-gray-900 tracking-tight\">\n          Page Not Found\n        </h1>\n        <p className=\"text-base text-gray-500\">\n          The page you are looking for might have been removed, had its name\n          changed, or is temporarily unavailable.\n        </p>\n        <Link\n          href=\"/\"\n          className=\"max-w-48 mx-auto flex justify-center py-2 px-4 border border-gray-300 rounded-full shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-orange-500\"\n        >\n          Back to Home\n        </Link>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "components/ui/avatar.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Avatar as AvatarPrimitive } from \"radix-ui\";;\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Avatar({\n  className,\n  ...props\n}: React.ComponentProps<typeof AvatarPrimitive.Root>) {\n  return (\n    <AvatarPrimitive.Root\n      data-slot=\"avatar\"\n      className={cn(\n        \"relative flex size-8 shrink-0 overflow-hidden rounded-full\",\n        className\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction AvatarImage({\n  className,\n  ...props\n}: React.ComponentProps<typeof AvatarPrimitive.Image>) {\n  return (\n    <AvatarPrimitive.Image\n      data-slot=\"avatar-image\"\n      className={cn(\"aspect-square size-full\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction AvatarFallback({\n  className,\n  ...props\n}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {\n  return (\n    <AvatarPrimitive.Fallback\n      data-slot=\"avatar-fallback\"\n      className={cn(\n        \"bg-muted flex size-full items-center justify-center rounded-full\",\n        className\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Avatar, AvatarImage, AvatarFallback };\n"
  },
  {
    "path": "components/ui/button.tsx",
    "content": "import * as React from \"react\";\nimport { Slot as SlotPrimitive } from \"radix-ui\";;\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst buttonVariants = cva(\n  \"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive\",\n  {\n    variants: {\n      variant: {\n        default:\n          \"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90\",\n        destructive:\n          \"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60\",\n        outline:\n          \"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50\",\n        secondary:\n          \"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80\",\n        ghost:\n          \"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50\",\n        link: \"text-primary underline-offset-4 hover:underline\"\n      },\n      size: {\n        default: \"h-9 px-4 py-2 has-[>svg]:px-3\",\n        sm: \"h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5\",\n        lg: \"h-10 rounded-md px-6 has-[>svg]:px-4\",\n        icon: \"size-9\"\n      }\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\"\n    }\n  }\n);\n\nfunction Button({\n  className,\n  variant,\n  size,\n  asChild = false,\n  ...props\n}: React.ComponentProps<\"button\"> &\n  VariantProps<typeof buttonVariants> & {\n    asChild?: boolean;\n  }) {\n  const Comp = asChild ? SlotPrimitive.Slot : \"button\";\n\n  return (\n    <Comp\n      data-slot=\"button\"\n      className={cn(buttonVariants({ variant, size, className }))}\n      {...props}\n    />\n  );\n}\n\nexport { Button, buttonVariants };\n"
  },
  {
    "path": "components/ui/card.tsx",
    "content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Card({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card\"\n      className={cn(\n        \"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm\",\n        className\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction CardHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-header\"\n      className={cn(\n        \"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6\",\n        className\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction CardTitle({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-title\"\n      className={cn(\"leading-none font-semibold\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction CardDescription({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-description\"\n      className={cn(\"text-muted-foreground text-sm\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction CardAction({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-action\"\n      className={cn(\n        \"col-start-2 row-span-2 row-start-1 self-start justify-self-end\",\n        className\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction CardContent({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-content\"\n      className={cn(\"px-6\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction CardFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-footer\"\n      className={cn(\"flex items-center px-6 [.border-t]:pt-6\", className)}\n      {...props}\n    />\n  );\n}\n\nexport {\n  Card,\n  CardHeader,\n  CardFooter,\n  CardTitle,\n  CardAction,\n  CardDescription,\n  CardContent\n};\n"
  },
  {
    "path": "components/ui/dropdown-menu.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { DropdownMenu as DropdownMenuPrimitive } from \"radix-ui\";;\nimport { CheckIcon, ChevronRightIcon, CircleIcon } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction DropdownMenu({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {\n  return <DropdownMenuPrimitive.Root data-slot=\"dropdown-menu\" {...props} />;\n}\n\nfunction DropdownMenuPortal({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {\n  return (\n    <DropdownMenuPrimitive.Portal data-slot=\"dropdown-menu-portal\" {...props} />\n  );\n}\n\nfunction DropdownMenuTrigger({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {\n  return (\n    <DropdownMenuPrimitive.Trigger\n      data-slot=\"dropdown-menu-trigger\"\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuContent({\n  className,\n  sideOffset = 4,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {\n  return (\n    <DropdownMenuPrimitive.Portal>\n      <DropdownMenuPrimitive.Content\n        data-slot=\"dropdown-menu-content\"\n        sideOffset={sideOffset}\n        className={cn(\n          \"bg-popover text-popover-foreground 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 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md\",\n          className\n        )}\n        {...props}\n      />\n    </DropdownMenuPrimitive.Portal>\n  );\n}\n\nfunction DropdownMenuGroup({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {\n  return (\n    <DropdownMenuPrimitive.Group data-slot=\"dropdown-menu-group\" {...props} />\n  );\n}\n\nfunction DropdownMenuItem({\n  className,\n  inset,\n  variant = \"default\",\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {\n  inset?: boolean;\n  variant?: \"default\" | \"destructive\";\n}) {\n  return (\n    <DropdownMenuPrimitive.Item\n      data-slot=\"dropdown-menu-item\"\n      data-inset={inset}\n      data-variant={variant}\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuCheckboxItem({\n  className,\n  children,\n  checked,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {\n  return (\n    <DropdownMenuPrimitive.CheckboxItem\n      data-slot=\"dropdown-menu-checkbox-item\"\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className\n      )}\n      checked={checked}\n      {...props}\n    >\n      <span className=\"pointer-events-none absolute left-2 flex size-3.5 items-center justify-center\">\n        <DropdownMenuPrimitive.ItemIndicator>\n          <CheckIcon className=\"size-4\" />\n        </DropdownMenuPrimitive.ItemIndicator>\n      </span>\n      {children}\n    </DropdownMenuPrimitive.CheckboxItem>\n  );\n}\n\nfunction DropdownMenuRadioGroup({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {\n  return (\n    <DropdownMenuPrimitive.RadioGroup\n      data-slot=\"dropdown-menu-radio-group\"\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuRadioItem({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {\n  return (\n    <DropdownMenuPrimitive.RadioItem\n      data-slot=\"dropdown-menu-radio-item\"\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className\n      )}\n      {...props}\n    >\n      <span className=\"pointer-events-none absolute left-2 flex size-3.5 items-center justify-center\">\n        <DropdownMenuPrimitive.ItemIndicator>\n          <CircleIcon className=\"size-2 fill-current\" />\n        </DropdownMenuPrimitive.ItemIndicator>\n      </span>\n      {children}\n    </DropdownMenuPrimitive.RadioItem>\n  );\n}\n\nfunction DropdownMenuLabel({\n  className,\n  inset,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {\n  inset?: boolean;\n}) {\n  return (\n    <DropdownMenuPrimitive.Label\n      data-slot=\"dropdown-menu-label\"\n      data-inset={inset}\n      className={cn(\n        \"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8\",\n        className\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuSeparator({\n  className,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {\n  return (\n    <DropdownMenuPrimitive.Separator\n      data-slot=\"dropdown-menu-separator\"\n      className={cn(\"bg-border -mx-1 my-1 h-px\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuShortcut({\n  className,\n  ...props\n}: React.ComponentProps<\"span\">) {\n  return (\n    <span\n      data-slot=\"dropdown-menu-shortcut\"\n      className={cn(\n        \"text-muted-foreground ml-auto text-xs tracking-widest\",\n        className\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuSub({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {\n  return <DropdownMenuPrimitive.Sub data-slot=\"dropdown-menu-sub\" {...props} />;\n}\n\nfunction DropdownMenuSubTrigger({\n  className,\n  inset,\n  children,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {\n  inset?: boolean;\n}) {\n  return (\n    <DropdownMenuPrimitive.SubTrigger\n      data-slot=\"dropdown-menu-sub-trigger\"\n      data-inset={inset}\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8\",\n        className\n      )}\n      {...props}\n    >\n      {children}\n      <ChevronRightIcon className=\"ml-auto size-4\" />\n    </DropdownMenuPrimitive.SubTrigger>\n  );\n}\n\nfunction DropdownMenuSubContent({\n  className,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {\n  return (\n    <DropdownMenuPrimitive.SubContent\n      data-slot=\"dropdown-menu-sub-content\"\n      className={cn(\n        \"bg-popover text-popover-foreground 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 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg\",\n        className\n      )}\n      {...props}\n    />\n  );\n}\n\nexport {\n  DropdownMenu,\n  DropdownMenuPortal,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuLabel,\n  DropdownMenuItem,\n  DropdownMenuCheckboxItem,\n  DropdownMenuRadioGroup,\n  DropdownMenuRadioItem,\n  DropdownMenuSeparator,\n  DropdownMenuShortcut,\n  DropdownMenuSub,\n  DropdownMenuSubTrigger,\n  DropdownMenuSubContent\n};\n"
  },
  {
    "path": "components/ui/input.tsx",
    "content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Input({ className, type, ...props }: React.ComponentProps<\"input\">) {\n  return (\n    <input\n      type={type}\n      data-slot=\"input\"\n      className={cn(\n        \"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm\",\n        \"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]\",\n        \"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive\",\n        className\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Input };\n"
  },
  {
    "path": "components/ui/label.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Label as LabelPrimitive } from \"radix-ui\";;\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Label({\n  className,\n  ...props\n}: React.ComponentProps<typeof LabelPrimitive.Root>) {\n  return (\n    <LabelPrimitive.Root\n      data-slot=\"label\"\n      className={cn(\n        \"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50\",\n        className\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Label };\n"
  },
  {
    "path": "components/ui/radio-group.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { RadioGroup as RadioGroupPrimitive } from \"radix-ui\";;\nimport { CircleIcon } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction RadioGroup({\n  className,\n  ...props\n}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {\n  return (\n    <RadioGroupPrimitive.Root\n      data-slot=\"radio-group\"\n      className={cn(\"grid gap-3\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction RadioGroupItem({\n  className,\n  ...props\n}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {\n  return (\n    <RadioGroupPrimitive.Item\n      data-slot=\"radio-group-item\"\n      className={cn(\n        \"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50\",\n        className\n      )}\n      {...props}\n    >\n      <RadioGroupPrimitive.Indicator\n        data-slot=\"radio-group-indicator\"\n        className=\"relative flex items-center justify-center\"\n      >\n        <CircleIcon className=\"fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2\" />\n      </RadioGroupPrimitive.Indicator>\n    </RadioGroupPrimitive.Item>\n  );\n}\n\nexport { RadioGroup, RadioGroupItem };\n"
  },
  {
    "path": "components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"new-york\",\n  \"rsc\": true,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"config\": \"\",\n    \"css\": \"app/globals.css\",\n    \"baseColor\": \"zinc\",\n    \"cssVariables\": true,\n    \"prefix\": \"\"\n  },\n  \"aliases\": {\n    \"components\": \"@/components\",\n    \"utils\": \"@/lib/utils\",\n    \"ui\": \"@/components/ui\",\n    \"lib\": \"@/lib\",\n    \"hooks\": \"@/hooks\"\n  },\n  \"iconLibrary\": \"lucide\"\n}"
  },
  {
    "path": "drizzle.config.ts",
    "content": "import type { Config } from 'drizzle-kit';\n\nexport default {\n  schema: './lib/db/schema.ts',\n  out: './lib/db/migrations',\n  dialect: 'postgresql',\n  dbCredentials: {\n    url: process.env.POSTGRES_URL!,\n  },\n} satisfies Config;\n"
  },
  {
    "path": "lib/auth/middleware.ts",
    "content": "import { z } from 'zod';\nimport { TeamDataWithMembers, User } from '@/lib/db/schema';\nimport { getTeamForUser, getUser } from '@/lib/db/queries';\nimport { redirect } from 'next/navigation';\n\nexport type ActionState = {\n  error?: string;\n  success?: string;\n  [key: string]: any; // This allows for additional properties\n};\n\ntype ValidatedActionFunction<S extends z.ZodType<any, any>, T> = (\n  data: z.infer<S>,\n  formData: FormData\n) => Promise<T>;\n\nexport function validatedAction<S extends z.ZodType<any, any>, T>(\n  schema: S,\n  action: ValidatedActionFunction<S, T>\n) {\n  return async (prevState: ActionState, formData: FormData) => {\n    const result = schema.safeParse(Object.fromEntries(formData));\n    if (!result.success) {\n      return { error: result.error.errors[0].message };\n    }\n\n    return action(result.data, formData);\n  };\n}\n\ntype ValidatedActionWithUserFunction<S extends z.ZodType<any, any>, T> = (\n  data: z.infer<S>,\n  formData: FormData,\n  user: User\n) => Promise<T>;\n\nexport function validatedActionWithUser<S extends z.ZodType<any, any>, T>(\n  schema: S,\n  action: ValidatedActionWithUserFunction<S, T>\n) {\n  return async (prevState: ActionState, formData: FormData) => {\n    const user = await getUser();\n    if (!user) {\n      throw new Error('User is not authenticated');\n    }\n\n    const result = schema.safeParse(Object.fromEntries(formData));\n    if (!result.success) {\n      return { error: result.error.errors[0].message };\n    }\n\n    return action(result.data, formData, user);\n  };\n}\n\ntype ActionWithTeamFunction<T> = (\n  formData: FormData,\n  team: TeamDataWithMembers\n) => Promise<T>;\n\nexport function withTeam<T>(action: ActionWithTeamFunction<T>) {\n  return async (formData: FormData): Promise<T> => {\n    const user = await getUser();\n    if (!user) {\n      redirect('/sign-in');\n    }\n\n    const team = await getTeamForUser();\n    if (!team) {\n      throw new Error('Team not found');\n    }\n\n    return action(formData, team);\n  };\n}\n"
  },
  {
    "path": "lib/auth/session.ts",
    "content": "import { compare, hash } from 'bcryptjs';\nimport { SignJWT, jwtVerify } from 'jose';\nimport { cookies } from 'next/headers';\nimport { NewUser } from '@/lib/db/schema';\n\nconst key = new TextEncoder().encode(process.env.AUTH_SECRET);\nconst SALT_ROUNDS = 10;\n\nexport async function hashPassword(password: string) {\n  return hash(password, SALT_ROUNDS);\n}\n\nexport async function comparePasswords(\n  plainTextPassword: string,\n  hashedPassword: string\n) {\n  return compare(plainTextPassword, hashedPassword);\n}\n\ntype SessionData = {\n  user: { id: number };\n  expires: string;\n};\n\nexport async function signToken(payload: SessionData) {\n  return await new SignJWT(payload)\n    .setProtectedHeader({ alg: 'HS256' })\n    .setIssuedAt()\n    .setExpirationTime('1 day from now')\n    .sign(key);\n}\n\nexport async function verifyToken(input: string) {\n  const { payload } = await jwtVerify(input, key, {\n    algorithms: ['HS256'],\n  });\n  return payload as SessionData;\n}\n\nexport async function getSession() {\n  const session = (await cookies()).get('session')?.value;\n  if (!session) return null;\n  return await verifyToken(session);\n}\n\nexport async function setSession(user: NewUser) {\n  const expiresInOneDay = new Date(Date.now() + 24 * 60 * 60 * 1000);\n  const session: SessionData = {\n    user: { id: user.id! },\n    expires: expiresInOneDay.toISOString(),\n  };\n  const encryptedSession = await signToken(session);\n  (await cookies()).set('session', encryptedSession, {\n    expires: expiresInOneDay,\n    httpOnly: true,\n    secure: true,\n    sameSite: 'lax',\n  });\n}\n"
  },
  {
    "path": "lib/db/drizzle.ts",
    "content": "import { drizzle } from 'drizzle-orm/postgres-js';\nimport postgres from 'postgres';\nimport * as schema from './schema';\nimport dotenv from 'dotenv';\n\ndotenv.config();\n\nif (!process.env.POSTGRES_URL) {\n  throw new Error('POSTGRES_URL environment variable is not set');\n}\n\nexport const client = postgres(process.env.POSTGRES_URL);\nexport const db = drizzle(client, { schema });\n"
  },
  {
    "path": "lib/db/migrations/0000_soft_the_anarchist.sql",
    "content": "CREATE TABLE IF NOT EXISTS \"activity_logs\" (\n\t\"id\" serial PRIMARY KEY NOT NULL,\n\t\"team_id\" integer NOT NULL,\n\t\"user_id\" integer,\n\t\"action\" text NOT NULL,\n\t\"timestamp\" timestamp DEFAULT now() NOT NULL,\n\t\"ip_address\" varchar(45)\n);\n--> statement-breakpoint\nCREATE TABLE IF NOT EXISTS \"invitations\" (\n\t\"id\" serial PRIMARY KEY NOT NULL,\n\t\"team_id\" integer NOT NULL,\n\t\"email\" varchar(255) NOT NULL,\n\t\"role\" varchar(50) NOT NULL,\n\t\"invited_by\" integer NOT NULL,\n\t\"invited_at\" timestamp DEFAULT now() NOT NULL,\n\t\"status\" varchar(20) DEFAULT 'pending' NOT NULL\n);\n--> statement-breakpoint\nCREATE TABLE IF NOT EXISTS \"team_members\" (\n\t\"id\" serial PRIMARY KEY NOT NULL,\n\t\"user_id\" integer NOT NULL,\n\t\"team_id\" integer NOT NULL,\n\t\"role\" varchar(50) NOT NULL,\n\t\"joined_at\" timestamp DEFAULT now() NOT NULL\n);\n--> statement-breakpoint\nCREATE TABLE IF NOT EXISTS \"teams\" (\n\t\"id\" serial PRIMARY KEY NOT NULL,\n\t\"name\" varchar(100) NOT NULL,\n\t\"created_at\" timestamp DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp DEFAULT now() NOT NULL,\n\t\"stripe_customer_id\" text,\n\t\"stripe_subscription_id\" text,\n\t\"stripe_product_id\" text,\n\t\"plan_name\" varchar(50),\n\t\"subscription_status\" varchar(20),\n\tCONSTRAINT \"teams_stripe_customer_id_unique\" UNIQUE(\"stripe_customer_id\"),\n\tCONSTRAINT \"teams_stripe_subscription_id_unique\" UNIQUE(\"stripe_subscription_id\")\n);\n--> statement-breakpoint\nCREATE TABLE IF NOT EXISTS \"users\" (\n\t\"id\" serial PRIMARY KEY NOT NULL,\n\t\"name\" varchar(100),\n\t\"email\" varchar(255) NOT NULL,\n\t\"password_hash\" text NOT NULL,\n\t\"role\" varchar(20) DEFAULT 'member' NOT NULL,\n\t\"created_at\" timestamp DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp DEFAULT now() NOT NULL,\n\t\"deleted_at\" timestamp,\n\tCONSTRAINT \"users_email_unique\" UNIQUE(\"email\")\n);\n--> statement-breakpoint\nDO $$ BEGIN\n ALTER TABLE \"activity_logs\" ADD CONSTRAINT \"activity_logs_team_id_teams_id_fk\" FOREIGN KEY (\"team_id\") REFERENCES \"public\".\"teams\"(\"id\") ON DELETE no action ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n--> statement-breakpoint\nDO $$ BEGIN\n ALTER TABLE \"activity_logs\" ADD CONSTRAINT \"activity_logs_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE no action ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n--> statement-breakpoint\nDO $$ BEGIN\n ALTER TABLE \"invitations\" ADD CONSTRAINT \"invitations_team_id_teams_id_fk\" FOREIGN KEY (\"team_id\") REFERENCES \"public\".\"teams\"(\"id\") ON DELETE no action ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n--> statement-breakpoint\nDO $$ BEGIN\n ALTER TABLE \"invitations\" ADD CONSTRAINT \"invitations_invited_by_users_id_fk\" FOREIGN KEY (\"invited_by\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE no action ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n--> statement-breakpoint\nDO $$ BEGIN\n ALTER TABLE \"team_members\" ADD CONSTRAINT \"team_members_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE no action ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n--> statement-breakpoint\nDO $$ BEGIN\n ALTER TABLE \"team_members\" ADD CONSTRAINT \"team_members_team_id_teams_id_fk\" FOREIGN KEY (\"team_id\") REFERENCES \"public\".\"teams\"(\"id\") ON DELETE no action ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n"
  },
  {
    "path": "lib/db/migrations/meta/0000_snapshot.json",
    "content": "{\n  \"id\": \"261fd993-fb2c-43e7-89d6-cd58786c5f58\",\n  \"prevId\": \"00000000-0000-0000-0000-000000000000\",\n  \"version\": \"7\",\n  \"dialect\": \"postgresql\",\n  \"tables\": {\n    \"public.activity_logs\": {\n      \"name\": \"activity_logs\",\n      \"schema\": \"\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"serial\",\n          \"primaryKey\": true,\n          \"notNull\": true\n        },\n        \"team_id\": {\n          \"name\": \"team_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true\n        },\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false\n        },\n        \"action\": {\n          \"name\": \"action\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true\n        },\n        \"timestamp\": {\n          \"name\": \"timestamp\",\n          \"type\": \"timestamp\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"default\": \"now()\"\n        },\n        \"ip_address\": {\n          \"name\": \"ip_address\",\n          \"type\": \"varchar(45)\",\n          \"primaryKey\": false,\n          \"notNull\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"activity_logs_team_id_teams_id_fk\": {\n          \"name\": \"activity_logs_team_id_teams_id_fk\",\n          \"tableFrom\": \"activity_logs\",\n          \"tableTo\": \"teams\",\n          \"columnsFrom\": [\n            \"team_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"activity_logs_user_id_users_id_fk\": {\n          \"name\": \"activity_logs_user_id_users_id_fk\",\n          \"tableFrom\": \"activity_logs\",\n          \"tableTo\": \"users\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"public.invitations\": {\n      \"name\": \"invitations\",\n      \"schema\": \"\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"serial\",\n          \"primaryKey\": true,\n          \"notNull\": true\n        },\n        \"team_id\": {\n          \"name\": \"team_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"varchar(255)\",\n          \"primaryKey\": false,\n          \"notNull\": true\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"varchar(50)\",\n          \"primaryKey\": false,\n          \"notNull\": true\n        },\n        \"invited_by\": {\n          \"name\": \"invited_by\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true\n        },\n        \"invited_at\": {\n          \"name\": \"invited_at\",\n          \"type\": \"timestamp\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"default\": \"now()\"\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"varchar(20)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"default\": \"'pending'\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"invitations_team_id_teams_id_fk\": {\n          \"name\": \"invitations_team_id_teams_id_fk\",\n          \"tableFrom\": \"invitations\",\n          \"tableTo\": \"teams\",\n          \"columnsFrom\": [\n            \"team_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"invitations_invited_by_users_id_fk\": {\n          \"name\": \"invitations_invited_by_users_id_fk\",\n          \"tableFrom\": \"invitations\",\n          \"tableTo\": \"users\",\n          \"columnsFrom\": [\n            \"invited_by\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"public.team_members\": {\n      \"name\": \"team_members\",\n      \"schema\": \"\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"serial\",\n          \"primaryKey\": true,\n          \"notNull\": true\n        },\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true\n        },\n        \"team_id\": {\n          \"name\": \"team_id\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"varchar(50)\",\n          \"primaryKey\": false,\n          \"notNull\": true\n        },\n        \"joined_at\": {\n          \"name\": \"joined_at\",\n          \"type\": \"timestamp\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"default\": \"now()\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"team_members_user_id_users_id_fk\": {\n          \"name\": \"team_members_user_id_users_id_fk\",\n          \"tableFrom\": \"team_members\",\n          \"tableTo\": \"users\",\n          \"columnsFrom\": [\n            \"user_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        },\n        \"team_members_team_id_teams_id_fk\": {\n          \"name\": \"team_members_team_id_teams_id_fk\",\n          \"tableFrom\": \"team_members\",\n          \"tableTo\": \"teams\",\n          \"columnsFrom\": [\n            \"team_id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"no action\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"public.teams\": {\n      \"name\": \"teams\",\n      \"schema\": \"\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"serial\",\n          \"primaryKey\": true,\n          \"notNull\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"varchar(100)\",\n          \"primaryKey\": false,\n          \"notNull\": true\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"timestamp\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"default\": \"now()\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"timestamp\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"default\": \"now()\"\n        },\n        \"stripe_customer_id\": {\n          \"name\": \"stripe_customer_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false\n        },\n        \"stripe_subscription_id\": {\n          \"name\": \"stripe_subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false\n        },\n        \"stripe_product_id\": {\n          \"name\": \"stripe_product_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false\n        },\n        \"plan_name\": {\n          \"name\": \"plan_name\",\n          \"type\": \"varchar(50)\",\n          \"primaryKey\": false,\n          \"notNull\": false\n        },\n        \"subscription_status\": {\n          \"name\": \"subscription_status\",\n          \"type\": \"varchar(20)\",\n          \"primaryKey\": false,\n          \"notNull\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {\n        \"teams_stripe_customer_id_unique\": {\n          \"name\": \"teams_stripe_customer_id_unique\",\n          \"nullsNotDistinct\": false,\n          \"columns\": [\n            \"stripe_customer_id\"\n          ]\n        },\n        \"teams_stripe_subscription_id_unique\": {\n          \"name\": \"teams_stripe_subscription_id_unique\",\n          \"nullsNotDistinct\": false,\n          \"columns\": [\n            \"stripe_subscription_id\"\n          ]\n        }\n      }\n    },\n    \"public.users\": {\n      \"name\": \"users\",\n      \"schema\": \"\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"serial\",\n          \"primaryKey\": true,\n          \"notNull\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"varchar(100)\",\n          \"primaryKey\": false,\n          \"notNull\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"varchar(255)\",\n          \"primaryKey\": false,\n          \"notNull\": true\n        },\n        \"password_hash\": {\n          \"name\": \"password_hash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"varchar(20)\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"default\": \"'member'\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"timestamp\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"default\": \"now()\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"timestamp\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"default\": \"now()\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"timestamp\",\n          \"primaryKey\": false,\n          \"notNull\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {\n        \"users_email_unique\": {\n          \"name\": \"users_email_unique\",\n          \"nullsNotDistinct\": false,\n          \"columns\": [\n            \"email\"\n          ]\n        }\n      }\n    }\n  },\n  \"enums\": {},\n  \"schemas\": {},\n  \"sequences\": {},\n  \"_meta\": {\n    \"columns\": {},\n    \"schemas\": {},\n    \"tables\": {}\n  }\n}"
  },
  {
    "path": "lib/db/migrations/meta/_journal.json",
    "content": "{\n  \"version\": \"7\",\n  \"dialect\": \"postgresql\",\n  \"entries\": [\n    {\n      \"idx\": 0,\n      \"version\": \"7\",\n      \"when\": 1726443359662,\n      \"tag\": \"0000_soft_the_anarchist\",\n      \"breakpoints\": true\n    }\n  ]\n}"
  },
  {
    "path": "lib/db/queries.ts",
    "content": "import { desc, and, eq, isNull } from 'drizzle-orm';\nimport { db } from './drizzle';\nimport { activityLogs, teamMembers, teams, users } from './schema';\nimport { cookies } from 'next/headers';\nimport { verifyToken } from '@/lib/auth/session';\n\nexport async function getUser() {\n  const sessionCookie = (await cookies()).get('session');\n  if (!sessionCookie || !sessionCookie.value) {\n    return null;\n  }\n\n  const sessionData = await verifyToken(sessionCookie.value);\n  if (\n    !sessionData ||\n    !sessionData.user ||\n    typeof sessionData.user.id !== 'number'\n  ) {\n    return null;\n  }\n\n  if (new Date(sessionData.expires) < new Date()) {\n    return null;\n  }\n\n  const user = await db\n    .select()\n    .from(users)\n    .where(and(eq(users.id, sessionData.user.id), isNull(users.deletedAt)))\n    .limit(1);\n\n  if (user.length === 0) {\n    return null;\n  }\n\n  return user[0];\n}\n\nexport async function getTeamByStripeCustomerId(customerId: string) {\n  const result = await db\n    .select()\n    .from(teams)\n    .where(eq(teams.stripeCustomerId, customerId))\n    .limit(1);\n\n  return result.length > 0 ? result[0] : null;\n}\n\nexport async function updateTeamSubscription(\n  teamId: number,\n  subscriptionData: {\n    stripeSubscriptionId: string | null;\n    stripeProductId: string | null;\n    planName: string | null;\n    subscriptionStatus: string;\n  }\n) {\n  await db\n    .update(teams)\n    .set({\n      ...subscriptionData,\n      updatedAt: new Date()\n    })\n    .where(eq(teams.id, teamId));\n}\n\nexport async function getUserWithTeam(userId: number) {\n  const result = await db\n    .select({\n      user: users,\n      teamId: teamMembers.teamId\n    })\n    .from(users)\n    .leftJoin(teamMembers, eq(users.id, teamMembers.userId))\n    .where(eq(users.id, userId))\n    .limit(1);\n\n  return result[0];\n}\n\nexport async function getActivityLogs() {\n  const user = await getUser();\n  if (!user) {\n    throw new Error('User not authenticated');\n  }\n\n  return await db\n    .select({\n      id: activityLogs.id,\n      action: activityLogs.action,\n      timestamp: activityLogs.timestamp,\n      ipAddress: activityLogs.ipAddress,\n      userName: users.name\n    })\n    .from(activityLogs)\n    .leftJoin(users, eq(activityLogs.userId, users.id))\n    .where(eq(activityLogs.userId, user.id))\n    .orderBy(desc(activityLogs.timestamp))\n    .limit(10);\n}\n\nexport async function getTeamForUser() {\n  const user = await getUser();\n  if (!user) {\n    return null;\n  }\n\n  const result = await db.query.teamMembers.findFirst({\n    where: eq(teamMembers.userId, user.id),\n    with: {\n      team: {\n        with: {\n          teamMembers: {\n            with: {\n              user: {\n                columns: {\n                  id: true,\n                  name: true,\n                  email: true\n                }\n              }\n            }\n          }\n        }\n      }\n    }\n  });\n\n  return result?.team || null;\n}\n"
  },
  {
    "path": "lib/db/schema.ts",
    "content": "import {\n  pgTable,\n  serial,\n  varchar,\n  text,\n  timestamp,\n  integer,\n} from 'drizzle-orm/pg-core';\nimport { relations } from 'drizzle-orm';\n\nexport const users = pgTable('users', {\n  id: serial('id').primaryKey(),\n  name: varchar('name', { length: 100 }),\n  email: varchar('email', { length: 255 }).notNull().unique(),\n  passwordHash: text('password_hash').notNull(),\n  role: varchar('role', { length: 20 }).notNull().default('member'),\n  createdAt: timestamp('created_at').notNull().defaultNow(),\n  updatedAt: timestamp('updated_at').notNull().defaultNow(),\n  deletedAt: timestamp('deleted_at'),\n});\n\nexport const teams = pgTable('teams', {\n  id: serial('id').primaryKey(),\n  name: varchar('name', { length: 100 }).notNull(),\n  createdAt: timestamp('created_at').notNull().defaultNow(),\n  updatedAt: timestamp('updated_at').notNull().defaultNow(),\n  stripeCustomerId: text('stripe_customer_id').unique(),\n  stripeSubscriptionId: text('stripe_subscription_id').unique(),\n  stripeProductId: text('stripe_product_id'),\n  planName: varchar('plan_name', { length: 50 }),\n  subscriptionStatus: varchar('subscription_status', { length: 20 }),\n});\n\nexport const teamMembers = pgTable('team_members', {\n  id: serial('id').primaryKey(),\n  userId: integer('user_id')\n    .notNull()\n    .references(() => users.id),\n  teamId: integer('team_id')\n    .notNull()\n    .references(() => teams.id),\n  role: varchar('role', { length: 50 }).notNull(),\n  joinedAt: timestamp('joined_at').notNull().defaultNow(),\n});\n\nexport const activityLogs = pgTable('activity_logs', {\n  id: serial('id').primaryKey(),\n  teamId: integer('team_id')\n    .notNull()\n    .references(() => teams.id),\n  userId: integer('user_id').references(() => users.id),\n  action: text('action').notNull(),\n  timestamp: timestamp('timestamp').notNull().defaultNow(),\n  ipAddress: varchar('ip_address', { length: 45 }),\n});\n\nexport const invitations = pgTable('invitations', {\n  id: serial('id').primaryKey(),\n  teamId: integer('team_id')\n    .notNull()\n    .references(() => teams.id),\n  email: varchar('email', { length: 255 }).notNull(),\n  role: varchar('role', { length: 50 }).notNull(),\n  invitedBy: integer('invited_by')\n    .notNull()\n    .references(() => users.id),\n  invitedAt: timestamp('invited_at').notNull().defaultNow(),\n  status: varchar('status', { length: 20 }).notNull().default('pending'),\n});\n\nexport const teamsRelations = relations(teams, ({ many }) => ({\n  teamMembers: many(teamMembers),\n  activityLogs: many(activityLogs),\n  invitations: many(invitations),\n}));\n\nexport const usersRelations = relations(users, ({ many }) => ({\n  teamMembers: many(teamMembers),\n  invitationsSent: many(invitations),\n}));\n\nexport const invitationsRelations = relations(invitations, ({ one }) => ({\n  team: one(teams, {\n    fields: [invitations.teamId],\n    references: [teams.id],\n  }),\n  invitedBy: one(users, {\n    fields: [invitations.invitedBy],\n    references: [users.id],\n  }),\n}));\n\nexport const teamMembersRelations = relations(teamMembers, ({ one }) => ({\n  user: one(users, {\n    fields: [teamMembers.userId],\n    references: [users.id],\n  }),\n  team: one(teams, {\n    fields: [teamMembers.teamId],\n    references: [teams.id],\n  }),\n}));\n\nexport const activityLogsRelations = relations(activityLogs, ({ one }) => ({\n  team: one(teams, {\n    fields: [activityLogs.teamId],\n    references: [teams.id],\n  }),\n  user: one(users, {\n    fields: [activityLogs.userId],\n    references: [users.id],\n  }),\n}));\n\nexport type User = typeof users.$inferSelect;\nexport type NewUser = typeof users.$inferInsert;\nexport type Team = typeof teams.$inferSelect;\nexport type NewTeam = typeof teams.$inferInsert;\nexport type TeamMember = typeof teamMembers.$inferSelect;\nexport type NewTeamMember = typeof teamMembers.$inferInsert;\nexport type ActivityLog = typeof activityLogs.$inferSelect;\nexport type NewActivityLog = typeof activityLogs.$inferInsert;\nexport type Invitation = typeof invitations.$inferSelect;\nexport type NewInvitation = typeof invitations.$inferInsert;\nexport type TeamDataWithMembers = Team & {\n  teamMembers: (TeamMember & {\n    user: Pick<User, 'id' | 'name' | 'email'>;\n  })[];\n};\n\nexport enum ActivityType {\n  SIGN_UP = 'SIGN_UP',\n  SIGN_IN = 'SIGN_IN',\n  SIGN_OUT = 'SIGN_OUT',\n  UPDATE_PASSWORD = 'UPDATE_PASSWORD',\n  DELETE_ACCOUNT = 'DELETE_ACCOUNT',\n  UPDATE_ACCOUNT = 'UPDATE_ACCOUNT',\n  CREATE_TEAM = 'CREATE_TEAM',\n  REMOVE_TEAM_MEMBER = 'REMOVE_TEAM_MEMBER',\n  INVITE_TEAM_MEMBER = 'INVITE_TEAM_MEMBER',\n  ACCEPT_INVITATION = 'ACCEPT_INVITATION',\n}\n"
  },
  {
    "path": "lib/db/seed.ts",
    "content": "import { stripe } from '../payments/stripe';\nimport { db } from './drizzle';\nimport { users, teams, teamMembers } from './schema';\nimport { hashPassword } from '@/lib/auth/session';\n\nasync function createStripeProducts() {\n  console.log('Creating Stripe products and prices...');\n\n  const baseProduct = await stripe.products.create({\n    name: 'Base',\n    description: 'Base subscription plan',\n  });\n\n  await stripe.prices.create({\n    product: baseProduct.id,\n    unit_amount: 800, // $8 in cents\n    currency: 'usd',\n    recurring: {\n      interval: 'month',\n      trial_period_days: 7,\n    },\n  });\n\n  const plusProduct = await stripe.products.create({\n    name: 'Plus',\n    description: 'Plus subscription plan',\n  });\n\n  await stripe.prices.create({\n    product: plusProduct.id,\n    unit_amount: 1200, // $12 in cents\n    currency: 'usd',\n    recurring: {\n      interval: 'month',\n      trial_period_days: 7,\n    },\n  });\n\n  console.log('Stripe products and prices created successfully.');\n}\n\nasync function seed() {\n  const email = 'test@test.com';\n  const password = 'admin123';\n  const passwordHash = await hashPassword(password);\n\n  const [user] = await db\n    .insert(users)\n    .values([\n      {\n        email: email,\n        passwordHash: passwordHash,\n        role: \"owner\",\n      },\n    ])\n    .returning();\n\n  console.log('Initial user created.');\n\n  const [team] = await db\n    .insert(teams)\n    .values({\n      name: 'Test Team',\n    })\n    .returning();\n\n  await db.insert(teamMembers).values({\n    teamId: team.id,\n    userId: user.id,\n    role: 'owner',\n  });\n\n  await createStripeProducts();\n}\n\nseed()\n  .catch((error) => {\n    console.error('Seed process failed:', error);\n    process.exit(1);\n  })\n  .finally(() => {\n    console.log('Seed process finished. Exiting...');\n    process.exit(0);\n  });\n"
  },
  {
    "path": "lib/db/setup.ts",
    "content": "import { exec } from 'node:child_process';\nimport { promises as fs } from 'node:fs';\nimport { promisify } from 'node:util';\nimport readline from 'node:readline';\nimport crypto from 'node:crypto';\nimport path from 'node:path';\nimport os from 'node:os';\n\nconst execAsync = promisify(exec);\n\nfunction question(query: string): Promise<string> {\n  const rl = readline.createInterface({\n    input: process.stdin,\n    output: process.stdout,\n  });\n\n  return new Promise((resolve) =>\n    rl.question(query, (ans) => {\n      rl.close();\n      resolve(ans);\n    })\n  );\n}\n\nasync function checkStripeCLI() {\n  console.log(\n    'Step 1: Checking if Stripe CLI is installed and authenticated...'\n  );\n  try {\n    await execAsync('stripe --version');\n    console.log('Stripe CLI is installed.');\n\n    // Check if Stripe CLI is authenticated\n    try {\n      await execAsync('stripe config --list');\n      console.log('Stripe CLI is authenticated.');\n    } catch (error) {\n      console.log(\n        'Stripe CLI is not authenticated or the authentication has expired.'\n      );\n      console.log('Please run: stripe login');\n      const answer = await question(\n        'Have you completed the authentication? (y/n): '\n      );\n      if (answer.toLowerCase() !== 'y') {\n        console.log(\n          'Please authenticate with Stripe CLI and run this script again.'\n        );\n        process.exit(1);\n      }\n\n      // Verify authentication after user confirms login\n      try {\n        await execAsync('stripe config --list');\n        console.log('Stripe CLI authentication confirmed.');\n      } catch (error) {\n        console.error(\n          'Failed to verify Stripe CLI authentication. Please try again.'\n        );\n        process.exit(1);\n      }\n    }\n  } catch (error) {\n    console.error(\n      'Stripe CLI is not installed. Please install it and try again.'\n    );\n    console.log('To install Stripe CLI, follow these steps:');\n    console.log('1. Visit: https://docs.stripe.com/stripe-cli');\n    console.log(\n      '2. Download and install the Stripe CLI for your operating system'\n    );\n    console.log('3. After installation, run: stripe login');\n    console.log(\n      'After installation and authentication, please run this setup script again.'\n    );\n    process.exit(1);\n  }\n}\n\nasync function getPostgresURL(): Promise<string> {\n  console.log('Step 2: Setting up Postgres');\n  const dbChoice = await question(\n    'Do you want to use a local Postgres instance with Docker (L) or a remote Postgres instance (R)? (L/R): '\n  );\n\n  if (dbChoice.toLowerCase() === 'l') {\n    console.log('Setting up local Postgres instance with Docker...');\n    await setupLocalPostgres();\n    return 'postgres://postgres:postgres@localhost:54322/postgres';\n  } else {\n    console.log(\n      'You can find Postgres databases at: https://vercel.com/marketplace?category=databases'\n    );\n    return await question('Enter your POSTGRES_URL: ');\n  }\n}\n\nasync function setupLocalPostgres() {\n  console.log('Checking if Docker is installed...');\n  try {\n    await execAsync('docker --version');\n    console.log('Docker is installed.');\n  } catch (error) {\n    console.error(\n      'Docker is not installed. Please install Docker and try again.'\n    );\n    console.log(\n      'To install Docker, visit: https://docs.docker.com/get-docker/'\n    );\n    process.exit(1);\n  }\n\n  console.log('Creating docker-compose.yml file...');\n  const dockerComposeContent = `\nservices:\n  postgres:\n    image: postgres:16.4-alpine\n    container_name: next_saas_starter_postgres\n    environment:\n      POSTGRES_DB: postgres\n      POSTGRES_USER: postgres\n      POSTGRES_PASSWORD: postgres\n    ports:\n      - \"54322:5432\"\n    volumes:\n      - postgres_data:/var/lib/postgresql/data\n\nvolumes:\n  postgres_data:\n`;\n\n  await fs.writeFile(\n    path.join(process.cwd(), 'docker-compose.yml'),\n    dockerComposeContent\n  );\n  console.log('docker-compose.yml file created.');\n\n  console.log('Starting Docker container with `docker compose up -d`...');\n  try {\n    await execAsync('docker compose up -d');\n    console.log('Docker container started successfully.');\n  } catch (error) {\n    console.error(\n      'Failed to start Docker container. Please check your Docker installation and try again.'\n    );\n    process.exit(1);\n  }\n}\n\nasync function getStripeSecretKey(): Promise<string> {\n  console.log('Step 3: Getting Stripe Secret Key');\n  console.log(\n    'You can find your Stripe Secret Key at: https://dashboard.stripe.com/test/apikeys'\n  );\n  return await question('Enter your Stripe Secret Key: ');\n}\n\nasync function createStripeWebhook(): Promise<string> {\n  console.log('Step 4: Creating Stripe webhook...');\n  try {\n    const { stdout } = await execAsync('stripe listen --print-secret');\n    const match = stdout.match(/whsec_[a-zA-Z0-9]+/);\n    if (!match) {\n      throw new Error('Failed to extract Stripe webhook secret');\n    }\n    console.log('Stripe webhook created.');\n    return match[0];\n  } catch (error) {\n    console.error(\n      'Failed to create Stripe webhook. Check your Stripe CLI installation and permissions.'\n    );\n    if (os.platform() === 'win32') {\n      console.log(\n        'Note: On Windows, you may need to run this script as an administrator.'\n      );\n    }\n    throw error;\n  }\n}\n\nfunction generateAuthSecret(): string {\n  console.log('Step 5: Generating AUTH_SECRET...');\n  return crypto.randomBytes(32).toString('hex');\n}\n\nasync function writeEnvFile(envVars: Record<string, string>) {\n  console.log('Step 6: Writing environment variables to .env');\n  const envContent = Object.entries(envVars)\n    .map(([key, value]) => `${key}=${value}`)\n    .join('\\n');\n\n  await fs.writeFile(path.join(process.cwd(), '.env'), envContent);\n  console.log('.env file created with the necessary variables.');\n}\n\nasync function main() {\n  await checkStripeCLI();\n\n  const POSTGRES_URL = await getPostgresURL();\n  const STRIPE_SECRET_KEY = await getStripeSecretKey();\n  const STRIPE_WEBHOOK_SECRET = await createStripeWebhook();\n  const BASE_URL = 'http://localhost:3000';\n  const AUTH_SECRET = generateAuthSecret();\n\n  await writeEnvFile({\n    POSTGRES_URL,\n    STRIPE_SECRET_KEY,\n    STRIPE_WEBHOOK_SECRET,\n    BASE_URL,\n    AUTH_SECRET,\n  });\n\n  console.log('🎉 Setup completed successfully!');\n}\n\nmain().catch(console.error);\n"
  },
  {
    "path": "lib/payments/actions.ts",
    "content": "'use server';\n\nimport { redirect } from 'next/navigation';\nimport { createCheckoutSession, createCustomerPortalSession } from './stripe';\nimport { withTeam } from '@/lib/auth/middleware';\n\nexport const checkoutAction = withTeam(async (formData, team) => {\n  const priceId = formData.get('priceId') as string;\n  await createCheckoutSession({ team: team, priceId });\n});\n\nexport const customerPortalAction = withTeam(async (_, team) => {\n  const portalSession = await createCustomerPortalSession(team);\n  redirect(portalSession.url);\n});\n"
  },
  {
    "path": "lib/payments/stripe.ts",
    "content": "import Stripe from 'stripe';\nimport { redirect } from 'next/navigation';\nimport { Team } from '@/lib/db/schema';\nimport {\n  getTeamByStripeCustomerId,\n  getUser,\n  updateTeamSubscription\n} from '@/lib/db/queries';\n\nexport const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {\n  apiVersion: '2025-04-30.basil'\n});\n\nexport async function createCheckoutSession({\n  team,\n  priceId\n}: {\n  team: Team | null;\n  priceId: string;\n}) {\n  const user = await getUser();\n\n  if (!team || !user) {\n    redirect(`/sign-up?redirect=checkout&priceId=${priceId}`);\n  }\n\n  const session = await stripe.checkout.sessions.create({\n    payment_method_types: ['card'],\n    line_items: [\n      {\n        price: priceId,\n        quantity: 1\n      }\n    ],\n    mode: 'subscription',\n    success_url: `${process.env.BASE_URL}/api/stripe/checkout?session_id={CHECKOUT_SESSION_ID}`,\n    cancel_url: `${process.env.BASE_URL}/pricing`,\n    customer: team.stripeCustomerId || undefined,\n    client_reference_id: user.id.toString(),\n    allow_promotion_codes: true,\n    subscription_data: {\n      trial_period_days: 14\n    }\n  });\n\n  redirect(session.url!);\n}\n\nexport async function createCustomerPortalSession(team: Team) {\n  if (!team.stripeCustomerId || !team.stripeProductId) {\n    redirect('/pricing');\n  }\n\n  let configuration: Stripe.BillingPortal.Configuration;\n  const configurations = await stripe.billingPortal.configurations.list();\n\n  if (configurations.data.length > 0) {\n    configuration = configurations.data[0];\n  } else {\n    const product = await stripe.products.retrieve(team.stripeProductId);\n    if (!product.active) {\n      throw new Error(\"Team's product is not active in Stripe\");\n    }\n\n    const prices = await stripe.prices.list({\n      product: product.id,\n      active: true\n    });\n    if (prices.data.length === 0) {\n      throw new Error(\"No active prices found for the team's product\");\n    }\n\n    configuration = await stripe.billingPortal.configurations.create({\n      business_profile: {\n        headline: 'Manage your subscription'\n      },\n      features: {\n        subscription_update: {\n          enabled: true,\n          default_allowed_updates: ['price', 'quantity', 'promotion_code'],\n          proration_behavior: 'create_prorations',\n          products: [\n            {\n              product: product.id,\n              prices: prices.data.map((price) => price.id)\n            }\n          ]\n        },\n        subscription_cancel: {\n          enabled: true,\n          mode: 'at_period_end',\n          cancellation_reason: {\n            enabled: true,\n            options: [\n              'too_expensive',\n              'missing_features',\n              'switched_service',\n              'unused',\n              'other'\n            ]\n          }\n        },\n        payment_method_update: {\n          enabled: true\n        }\n      }\n    });\n  }\n\n  return stripe.billingPortal.sessions.create({\n    customer: team.stripeCustomerId,\n    return_url: `${process.env.BASE_URL}/dashboard`,\n    configuration: configuration.id\n  });\n}\n\nexport async function handleSubscriptionChange(\n  subscription: Stripe.Subscription\n) {\n  const customerId = subscription.customer as string;\n  const subscriptionId = subscription.id;\n  const status = subscription.status;\n\n  const team = await getTeamByStripeCustomerId(customerId);\n\n  if (!team) {\n    console.error('Team not found for Stripe customer:', customerId);\n    return;\n  }\n\n  if (status === 'active' || status === 'trialing') {\n    const plan = subscription.items.data[0]?.plan;\n    await updateTeamSubscription(team.id, {\n      stripeSubscriptionId: subscriptionId,\n      stripeProductId: plan?.product as string,\n      planName: (plan?.product as Stripe.Product).name,\n      subscriptionStatus: status\n    });\n  } else if (status === 'canceled' || status === 'unpaid') {\n    await updateTeamSubscription(team.id, {\n      stripeSubscriptionId: null,\n      stripeProductId: null,\n      planName: null,\n      subscriptionStatus: status\n    });\n  }\n}\n\nexport async function getStripePrices() {\n  const prices = await stripe.prices.list({\n    expand: ['data.product'],\n    active: true,\n    type: 'recurring'\n  });\n\n  return prices.data.map((price) => ({\n    id: price.id,\n    productId:\n      typeof price.product === 'string' ? price.product : price.product.id,\n    unitAmount: price.unit_amount,\n    currency: price.currency,\n    interval: price.recurring?.interval,\n    trialPeriodDays: price.recurring?.trial_period_days\n  }));\n}\n\nexport async function getStripeProducts() {\n  const products = await stripe.products.list({\n    active: true,\n    expand: ['data.default_price']\n  });\n\n  return products.data.map((product) => ({\n    id: product.id,\n    name: product.name,\n    description: product.description,\n    defaultPriceId:\n      typeof product.default_price === 'string'\n        ? product.default_price\n        : product.default_price?.id\n  }));\n}\n"
  },
  {
    "path": "lib/utils.ts",
    "content": "import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"
  },
  {
    "path": "middleware.ts",
    "content": "import { NextResponse } from 'next/server';\nimport type { NextRequest } from 'next/server';\nimport { signToken, verifyToken } from '@/lib/auth/session';\n\nconst protectedRoutes = '/dashboard';\n\nexport async function middleware(request: NextRequest) {\n  const { pathname } = request.nextUrl;\n  const sessionCookie = request.cookies.get('session');\n  const isProtectedRoute = pathname.startsWith(protectedRoutes);\n\n  if (isProtectedRoute && !sessionCookie) {\n    return NextResponse.redirect(new URL('/sign-in', request.url));\n  }\n\n  let res = NextResponse.next();\n\n  if (sessionCookie && request.method === 'GET') {\n    try {\n      const parsed = await verifyToken(sessionCookie.value);\n      const expiresInOneDay = new Date(Date.now() + 24 * 60 * 60 * 1000);\n\n      res.cookies.set({\n        name: 'session',\n        value: await signToken({\n          ...parsed,\n          expires: expiresInOneDay.toISOString()\n        }),\n        httpOnly: true,\n        secure: true,\n        sameSite: 'lax',\n        expires: expiresInOneDay\n      });\n    } catch (error) {\n      console.error('Error updating session:', error);\n      res.cookies.delete('session');\n      if (isProtectedRoute) {\n        return NextResponse.redirect(new URL('/sign-in', request.url));\n      }\n    }\n  }\n\n  return res;\n}\n\nexport const config = {\n  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],\n  runtime: 'nodejs'\n};\n"
  },
  {
    "path": "next.config.ts",
    "content": "import type { NextConfig } from 'next';\n\nconst nextConfig: NextConfig = {\n  experimental: {\n    ppr: true,\n    clientSegmentCache: true\n  }\n};\n\nexport default nextConfig;\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev --turbopack\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"db:setup\": \"npx tsx lib/db/setup.ts\",\n    \"db:seed\": \"npx tsx lib/db/seed.ts\",\n    \"db:generate\": \"drizzle-kit generate\",\n    \"db:migrate\": \"drizzle-kit migrate\",\n    \"db:studio\": \"drizzle-kit studio\"\n  },\n  \"dependencies\": {\n    \"@tailwindcss/postcss\": \"4.1.7\",\n    \"@types/node\": \"^22.15.18\",\n    \"@types/react\": \"19.1.4\",\n    \"@types/react-dom\": \"19.1.5\",\n    \"autoprefixer\": \"^10.4.21\",\n    \"bcryptjs\": \"^3.0.2\",\n    \"class-variance-authority\": \"^0.7.1\",\n    \"clsx\": \"^2.1.1\",\n    \"dotenv\": \"^16.5.0\",\n    \"drizzle-kit\": \"^0.31.1\",\n    \"drizzle-orm\": \"^0.43.1\",\n    \"jose\": \"^6.0.11\",\n    \"lucide-react\": \"^0.511.0\",\n    \"next\": \"15.6.0-canary.59\",\n    \"postcss\": \"^8.5.3\",\n    \"postgres\": \"^3.4.5\",\n    \"radix-ui\": \"^1.4.2\",\n    \"react\": \"19.1.0\",\n    \"react-dom\": \"19.1.0\",\n    \"server-only\": \"^0.0.1\",\n    \"stripe\": \"^18.1.0\",\n    \"swr\": \"^2.3.3\",\n    \"tailwind-merge\": \"^3.3.0\",\n    \"tailwindcss\": \"4.1.7\",\n    \"tw-animate-css\": \"^1.3.0\",\n    \"typescript\": \"^5.8.3\",\n    \"zod\": \"^3.24.4\"\n  }\n}\n"
  },
  {
    "path": "postcss.config.mjs",
    "content": "export default {\n  plugins: {\n    '@tailwindcss/postcss': {},\n  },\n};\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"lib\": [\n      \"dom\",\n      \"dom.iterable\",\n      \"esnext\"\n    ],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"incremental\": true,\n    \"baseUrl\": \".\",\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ],\n    \"paths\": {\n      \"@/*\": [\n        \"./*\"\n      ]\n    }\n  },\n  \"include\": [\n    \"next-env.d.ts\",\n    \"**/*.ts\",\n    \"**/*.tsx\",\n    \".next/types/**/*.ts\"\n  ],\n  \"exclude\": [\n    \"node_modules\"\n  ]\n}\n"
  }
]