[
  {
    "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.*\n.yarn/*\n!.yarn/patches\n!.yarn/plugins\n!.yarn/releases\n!.yarn/versions\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.pnpm-debug.log*\n\n# env files (can opt-in for committing if needed)\n.env*\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n"
  },
  {
    "path": "README.md",
    "content": "# Next.js Multi-Tenant Example\n\nA production-ready example of a multi-tenant application built with Next.js 15, featuring custom subdomains for each tenant.\n\n## Features\n\n- ✅ Custom subdomain routing with Next.js middleware\n- ✅ Tenant-specific content and pages\n- ✅ Shared components and layouts across tenants\n- ✅ Redis for tenant data storage\n- ✅ Admin interface for managing tenants\n- ✅ Emoji support for tenant branding\n- ✅ Support for local development with subdomains\n- ✅ Compatible with Vercel preview deployments\n\n## Tech Stack\n\n- [Next.js 15](https://nextjs.org/) with App Router\n- [React 19](https://react.dev/)\n- [Upstash Redis](https://upstash.com/) for data storage\n- [Tailwind 4](https://tailwindcss.com/) for styling\n- [shadcn/ui](https://ui.shadcn.com/) for the design system\n\n## Getting Started\n\n### Prerequisites\n\n- Node.js 18.17.0 or later\n- pnpm (recommended) or npm/yarn\n- Upstash Redis account (for production)\n\n### Installation\n\n1. Clone the repository:\n\n   ```bash\n   git clone https://github.com/vercel/platforms.git\n   cd platforms\n   ```\n\n2. Install dependencies:\n\n   ```bash\n   pnpm install\n   ```\n\n3. Set up environment variables:\n   Create a `.env.local` file in the root directory with:\n\n   ```\n   KV_REST_API_URL=your_redis_url\n   KV_REST_API_TOKEN=your_redis_token\n   ```\n\n4. Start the development server:\n\n   ```bash\n   pnpm dev\n   ```\n\n5. Access the application:\n   - Main site: http://localhost:3000\n   - Admin panel: http://localhost:3000/admin\n   - Tenants: http://[tenant-name].localhost:3000\n\n## Multi-Tenant Architecture\n\nThis application demonstrates a subdomain-based multi-tenant architecture where:\n\n- Each tenant gets their own subdomain (`tenant.yourdomain.com`)\n- The middleware handles routing requests to the correct tenant\n- Tenant data is stored in Redis using a `subdomain:{name}` key pattern\n- The main domain hosts the landing page and admin interface\n- Subdomains are dynamically mapped to tenant-specific content\n\nThe middleware (`middleware.ts`) intelligently detects subdomains across various environments (local development, production, and Vercel preview deployments).\n\n## Deployment\n\nThis application is designed to be deployed on Vercel. To deploy:\n\n1. Push your repository to GitHub\n2. Connect your repository to Vercel\n3. Configure environment variables\n4. Deploy\n\nFor custom domains, make sure to:\n\n1. Add your root domain to Vercel\n2. Set up a wildcard DNS record (`*.yourdomain.com`) on Vercel\n"
  },
  {
    "path": "app/actions.ts",
    "content": "'use server';\n\nimport { redis } from '@/lib/redis';\nimport { isValidIcon } from '@/lib/subdomains';\nimport { revalidatePath } from 'next/cache';\nimport { redirect } from 'next/navigation';\nimport { rootDomain, protocol } from '@/lib/utils';\n\nexport async function createSubdomainAction(\n  prevState: any,\n  formData: FormData\n) {\n  const subdomain = formData.get('subdomain') as string;\n  const icon = formData.get('icon') as string;\n\n  if (!subdomain || !icon) {\n    return { success: false, error: 'Subdomain and icon are required' };\n  }\n\n  if (!isValidIcon(icon)) {\n    return {\n      subdomain,\n      icon,\n      success: false,\n      error: 'Please enter a valid emoji (maximum 10 characters)'\n    };\n  }\n\n  const sanitizedSubdomain = subdomain.toLowerCase().replace(/[^a-z0-9-]/g, '');\n\n  if (sanitizedSubdomain !== subdomain) {\n    return {\n      subdomain,\n      icon,\n      success: false,\n      error:\n        'Subdomain can only have lowercase letters, numbers, and hyphens. Please try again.'\n    };\n  }\n\n  const subdomainAlreadyExists = await redis.get(\n    `subdomain:${sanitizedSubdomain}`\n  );\n  if (subdomainAlreadyExists) {\n    return {\n      subdomain,\n      icon,\n      success: false,\n      error: 'This subdomain is already taken'\n    };\n  }\n\n  await redis.set(`subdomain:${sanitizedSubdomain}`, {\n    emoji: icon,\n    createdAt: Date.now()\n  });\n\n  redirect(`${protocol}://${sanitizedSubdomain}.${rootDomain}`);\n}\n\nexport async function deleteSubdomainAction(\n  prevState: any,\n  formData: FormData\n) {\n  const subdomain = formData.get('subdomain');\n  await redis.del(`subdomain:${subdomain}`);\n  revalidatePath('/admin');\n  return { success: 'Domain deleted successfully' };\n}\n"
  },
  {
    "path": "app/admin/dashboard.tsx",
    "content": "'use client';\n\nimport { useActionState } from 'react';\nimport { Button } from '@/components/ui/button';\nimport { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';\nimport { Trash2, Loader2 } from 'lucide-react';\nimport Link from 'next/link';\nimport { deleteSubdomainAction } from '@/app/actions';\nimport { rootDomain, protocol } from '@/lib/utils';\n\ntype Tenant = {\n  subdomain: string;\n  emoji: string;\n  createdAt: number;\n};\n\ntype DeleteState = {\n  error?: string;\n  success?: string;\n};\n\nfunction DashboardHeader() {\n  // TODO: You can add authentication here with your preferred auth provider\n\n  return (\n    <div className=\"flex justify-between items-center mb-8\">\n      <h1 className=\"text-3xl font-bold\">Subdomain Management</h1>\n      <div className=\"flex items-center gap-4\">\n        <Link\n          href={`${protocol}://${rootDomain}`}\n          className=\"text-sm text-gray-500 hover:text-gray-700 transition-colors\"\n        >\n          {rootDomain}\n        </Link>\n      </div>\n    </div>\n  );\n}\n\nfunction TenantGrid({\n  tenants,\n  action,\n  isPending\n}: {\n  tenants: Tenant[];\n  action: (formData: FormData) => void;\n  isPending: boolean;\n}) {\n  if (tenants.length === 0) {\n    return (\n      <Card>\n        <CardContent className=\"py-8 text-center\">\n          <p className=\"text-gray-500\">No subdomains have been created yet.</p>\n        </CardContent>\n      </Card>\n    );\n  }\n\n  return (\n    <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-3\">\n      {tenants.map((tenant) => (\n        <Card key={tenant.subdomain}>\n          <CardHeader className=\"pb-2\">\n            <div className=\"flex items-center justify-between\">\n              <CardTitle className=\"text-xl\">{tenant.subdomain}</CardTitle>\n              <form action={action}>\n                <input\n                  type=\"hidden\"\n                  name=\"subdomain\"\n                  value={tenant.subdomain}\n                />\n                <Button\n                  variant=\"ghost\"\n                  size=\"icon\"\n                  type=\"submit\"\n                  disabled={isPending}\n                  className=\"text-gray-500 hover:text-gray-700 hover:bg-gray-50\"\n                >\n                  {isPending ? (\n                    <Loader2 className=\"h-5 w-5 animate-spin\" />\n                  ) : (\n                    <Trash2 className=\"h-5 w-5\" />\n                  )}\n                </Button>\n              </form>\n            </div>\n          </CardHeader>\n          <CardContent>\n            <div className=\"flex items-center justify-between\">\n              <div className=\"text-4xl\">{tenant.emoji}</div>\n              <div className=\"text-sm text-gray-500\">\n                Created: {new Date(tenant.createdAt).toLocaleDateString()}\n              </div>\n            </div>\n            <div className=\"mt-4\">\n              <a\n                href={`${protocol}://${tenant.subdomain}.${rootDomain}`}\n                target=\"_blank\"\n                rel=\"noopener noreferrer\"\n                className=\"text-blue-500 hover:underline text-sm\"\n              >\n                Visit subdomain →\n              </a>\n            </div>\n          </CardContent>\n        </Card>\n      ))}\n    </div>\n  );\n}\n\nexport function AdminDashboard({ tenants }: { tenants: Tenant[] }) {\n  const [state, action, isPending] = useActionState<DeleteState, FormData>(\n    deleteSubdomainAction,\n    {}\n  );\n\n  return (\n    <div className=\"space-y-6 relative p-4 md:p-8\">\n      <DashboardHeader />\n      <TenantGrid tenants={tenants} action={action} isPending={isPending} />\n\n      {state.error && (\n        <div className=\"fixed bottom-4 right-4 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded shadow-md\">\n          {state.error}\n        </div>\n      )}\n\n      {state.success && (\n        <div className=\"fixed bottom-4 right-4 bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded shadow-md\">\n          {state.success}\n        </div>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/admin/page.tsx",
    "content": "import { getAllSubdomains } from '@/lib/subdomains';\nimport type { Metadata } from 'next';\nimport { AdminDashboard } from './dashboard';\nimport { rootDomain } from '@/lib/utils';\n\nexport const metadata: Metadata = {\n  title: `Admin Dashboard | ${rootDomain}`,\n  description: `Manage subdomains for ${rootDomain}`\n};\n\nexport default async function AdminPage() {\n  // TODO: You can add authentication here with your preferred auth provider\n  const tenants = await getAllSubdomains();\n\n  return (\n    <div className=\"min-h-screen bg-gray-50 p-4 md:p-8\">\n      <AdminDashboard tenants={tenants} />\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/globals.css",
    "content": "@import \"tailwindcss\";\n@import \"tw-animate-css\";\n\n@custom-variant dark (&:is(.dark *));\n\n@theme inline {\n  --color-background: var(--background);\n  --color-foreground: var(--foreground);\n  --font-sans: var(--font-geist-sans);\n  --font-mono: var(--font-geist-mono);\n  --color-sidebar-ring: var(--sidebar-ring);\n  --color-sidebar-border: var(--sidebar-border);\n  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);\n  --color-sidebar-accent: var(--sidebar-accent);\n  --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);\n  --color-sidebar-primary: var(--sidebar-primary);\n  --color-sidebar-foreground: var(--sidebar-foreground);\n  --color-sidebar: var(--sidebar);\n  --color-chart-5: var(--chart-5);\n  --color-chart-4: var(--chart-4);\n  --color-chart-3: var(--chart-3);\n  --color-chart-2: var(--chart-2);\n  --color-chart-1: var(--chart-1);\n  --color-ring: var(--ring);\n  --color-input: var(--input);\n  --color-border: var(--border);\n  --color-destructive: var(--destructive);\n  --color-accent-foreground: var(--accent-foreground);\n  --color-accent: var(--accent);\n  --color-muted-foreground: var(--muted-foreground);\n  --color-muted: var(--muted);\n  --color-secondary-foreground: var(--secondary-foreground);\n  --color-secondary: var(--secondary);\n  --color-primary-foreground: var(--primary-foreground);\n  --color-primary: var(--primary);\n  --color-popover-foreground: var(--popover-foreground);\n  --color-popover: var(--popover);\n  --color-card-foreground: var(--card-foreground);\n  --color-card: var(--card);\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:root {\n  --radius: 0.625rem;\n  --background: oklch(1 0 0);\n  --foreground: oklch(0.141 0.005 285.823);\n  --card: oklch(1 0 0);\n  --card-foreground: oklch(0.141 0.005 285.823);\n  --popover: oklch(1 0 0);\n  --popover-foreground: oklch(0.141 0.005 285.823);\n  --primary: oklch(0.21 0.006 285.885);\n  --primary-foreground: oklch(0.985 0 0);\n  --secondary: oklch(0.967 0.001 286.375);\n  --secondary-foreground: oklch(0.21 0.006 285.885);\n  --muted: oklch(0.967 0.001 286.375);\n  --muted-foreground: oklch(0.552 0.016 285.938);\n  --accent: oklch(0.967 0.001 286.375);\n  --accent-foreground: oklch(0.21 0.006 285.885);\n  --destructive: oklch(0.577 0.245 27.325);\n  --border: oklch(0.92 0.004 286.32);\n  --input: oklch(0.92 0.004 286.32);\n  --ring: oklch(0.705 0.015 286.067);\n  --chart-1: oklch(0.646 0.222 41.116);\n  --chart-2: oklch(0.6 0.118 184.704);\n  --chart-3: oklch(0.398 0.07 227.392);\n  --chart-4: oklch(0.828 0.189 84.429);\n  --chart-5: oklch(0.769 0.188 70.08);\n  --sidebar: oklch(0.985 0 0);\n  --sidebar-foreground: oklch(0.141 0.005 285.823);\n  --sidebar-primary: oklch(0.21 0.006 285.885);\n  --sidebar-primary-foreground: oklch(0.985 0 0);\n  --sidebar-accent: oklch(0.967 0.001 286.375);\n  --sidebar-accent-foreground: oklch(0.21 0.006 285.885);\n  --sidebar-border: oklch(0.92 0.004 286.32);\n  --sidebar-ring: oklch(0.705 0.015 286.067);\n}\n\n.dark {\n  --background: oklch(0.141 0.005 285.823);\n  --foreground: oklch(0.985 0 0);\n  --card: oklch(0.21 0.006 285.885);\n  --card-foreground: oklch(0.985 0 0);\n  --popover: oklch(0.21 0.006 285.885);\n  --popover-foreground: oklch(0.985 0 0);\n  --primary: oklch(0.92 0.004 286.32);\n  --primary-foreground: oklch(0.21 0.006 285.885);\n  --secondary: oklch(0.274 0.006 286.033);\n  --secondary-foreground: oklch(0.985 0 0);\n  --muted: oklch(0.274 0.006 286.033);\n  --muted-foreground: oklch(0.705 0.015 286.067);\n  --accent: oklch(0.274 0.006 286.033);\n  --accent-foreground: oklch(0.985 0 0);\n  --destructive: oklch(0.704 0.191 22.216);\n  --border: oklch(1 0 0 / 10%);\n  --input: oklch(1 0 0 / 15%);\n  --ring: oklch(0.552 0.016 285.938);\n  --chart-1: oklch(0.488 0.243 264.376);\n  --chart-2: oklch(0.696 0.17 162.48);\n  --chart-3: oklch(0.769 0.188 70.08);\n  --chart-4: oklch(0.627 0.265 303.9);\n  --chart-5: oklch(0.645 0.246 16.439);\n  --sidebar: oklch(0.21 0.006 285.885);\n  --sidebar-foreground: oklch(0.985 0 0);\n  --sidebar-primary: oklch(0.488 0.243 264.376);\n  --sidebar-primary-foreground: oklch(0.985 0 0);\n  --sidebar-accent: oklch(0.274 0.006 286.033);\n  --sidebar-accent-foreground: oklch(0.985 0 0);\n  --sidebar-border: oklch(1 0 0 / 10%);\n  --sidebar-ring: oklch(0.552 0.016 285.938);\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\n.emoji-picker-container .epr-body::-webkit-scrollbar {\n  width: 8px;\n}\n\n.emoji-picker-container .epr-body::-webkit-scrollbar-track {\n  background: hsl(var(--background));\n}\n\n.emoji-picker-container .epr-body::-webkit-scrollbar-thumb {\n  background-color: hsl(var(--muted));\n  border-radius: 20px;\n}\n\n.emoji-picker-container .epr-body::-webkit-scrollbar-thumb:hover {\n  background-color: hsl(var(--muted-foreground) / 0.5);\n}\n\n.emoji-picker-container .epr-category-nav {\n  padding: 8px 0;\n}\n\n.emoji-picker-container .epr-header {\n  border-bottom: 1px solid hsl(var(--border));\n}\n\n.emoji-picker-container .epr-emoji-category-label {\n  background-color: hsl(var(--background));\n  font-size: 0.875rem;\n  color: hsl(var(--muted-foreground));\n  padding: 4px 8px;\n}\n\n.emoji-picker-container .epr-search {\n  margin: 8px;\n  border-radius: var(--radius);\n  border: 1px solid hsl(var(--input));\n  background-color: hsl(var(--background));\n}\n\n.emoji-picker-container .epr-search input {\n  border-radius: var(--radius);\n  background-color: transparent;\n  color: hsl(var(--foreground));\n}\n\n.emoji-picker-container .epr-emoji-category-content {\n  padding: 4px;\n}\n\n.emoji-picker-container .epr-body {\n  padding: 0;\n}\n\n.emoji-picker-container .epr-skin-tones {\n  border-radius: var(--radius);\n}\n\n.emoji-picker-container button.epr-emoji {\n  border-radius: var(--radius);\n}\n\n.emoji-picker-container button.epr-emoji:hover {\n  background-color: hsl(var(--accent));\n}\n\n.emoji-picker-container .epr-category-nav button {\n  opacity: 0.5;\n}\n\n.emoji-picker-container .epr-category-nav button.active {\n  opacity: 1;\n}\n\n.emoji-picker-container .epr-category-nav button:hover {\n  opacity: 0.8;\n}\n"
  },
  {
    "path": "app/layout.tsx",
    "content": "import type { Metadata } from 'next';\nimport { Geist } from 'next/font/google';\nimport { SpeedInsights } from '@vercel/speed-insights/next';\nimport './globals.css';\n\nconst geistSans = Geist({\n  variable: '--font-geist-sans',\n  subsets: ['latin']\n});\n\nexport const metadata: Metadata = {\n  title: 'Platforms Starter Kit',\n  description: 'Next.js template for building a multi-tenant SaaS.'\n};\n\nexport default function RootLayout({\n  children\n}: Readonly<{\n  children: React.ReactNode;\n}>) {\n  return (\n    <html lang=\"en\">\n      <body className={`${geistSans.variable} antialiased`}>\n        {children}\n        <SpeedInsights />\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "app/not-found.tsx",
    "content": "'use client';\n\nimport Link from 'next/link';\nimport { useEffect, useState } from 'react';\nimport { usePathname } from 'next/navigation';\nimport { rootDomain, protocol } from '@/lib/utils';\n\nexport default function NotFound() {\n  const [subdomain, setSubdomain] = useState<string | null>(null);\n  const pathname = usePathname();\n\n  useEffect(() => {\n    // Extract subdomain from URL if we're on a subdomain page\n    if (pathname?.startsWith('/subdomain/')) {\n      const extractedSubdomain = pathname.split('/')[2];\n      if (extractedSubdomain) {\n        setSubdomain(extractedSubdomain);\n      }\n    } else {\n      // Try to extract from hostname for direct subdomain access\n      const hostname = window.location.hostname;\n      if (hostname.includes(`.${rootDomain.split(':')[0]}`)) {\n        const extractedSubdomain = hostname.split('.')[0];\n        setSubdomain(extractedSubdomain);\n      }\n    }\n  }, [pathname]);\n\n  return (\n    <div className=\"flex min-h-screen flex-col items-center justify-center bg-gradient-to-b from-blue-50 to-white p-4\">\n      <div className=\"text-center\">\n        <h1 className=\"text-4xl font-bold tracking-tight text-gray-900\">\n          {subdomain ? (\n            <>\n              <span className=\"text-blue-600\">{subdomain}</span>.{rootDomain}{' '}\n              doesn't exist\n            </>\n          ) : (\n            'Subdomain Not Found'\n          )}\n        </h1>\n        <p className=\"mt-3 text-lg text-gray-600\">\n          This subdomain hasn't been created yet.\n        </p>\n        <div className=\"mt-6\">\n          <Link\n            href={`${protocol}://${rootDomain}`}\n            className=\"rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700\"\n          >\n            {subdomain ? `Create ${subdomain}` : `Go to ${rootDomain}`}\n          </Link>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/page.tsx",
    "content": "import Link from 'next/link';\nimport { SubdomainForm } from './subdomain-form';\nimport { rootDomain } from '@/lib/utils';\n\nexport default async function HomePage() {\n  return (\n    <div className=\"flex min-h-screen flex-col items-center justify-center bg-gradient-to-b from-blue-50 to-white p-4 relative\">\n      <div className=\"absolute top-4 right-4\">\n        <Link\n          href=\"/admin\"\n          className=\"text-sm text-gray-500 hover:text-gray-700 transition-colors\"\n        >\n          Admin\n        </Link>\n      </div>\n\n      <div className=\"w-full max-w-md space-y-8\">\n        <div className=\"text-center\">\n          <h1 className=\"text-4xl font-bold tracking-tight text-gray-900\">\n            {rootDomain}\n          </h1>\n          <p className=\"mt-3 text-lg text-gray-600\">\n            Create your own subdomain with a custom emoji\n          </p>\n        </div>\n\n        <div className=\"mt-8 bg-white shadow-md rounded-lg p-6\">\n          <SubdomainForm />\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/s/[subdomain]/page.tsx",
    "content": "import Link from 'next/link';\nimport type { Metadata } from 'next';\nimport { notFound } from 'next/navigation';\nimport { getSubdomainData } from '@/lib/subdomains';\nimport { protocol, rootDomain } from '@/lib/utils';\n\nexport async function generateMetadata({\n  params\n}: {\n  params: Promise<{ subdomain: string }>;\n}): Promise<Metadata> {\n  const { subdomain } = await params;\n  const subdomainData = await getSubdomainData(subdomain);\n\n  if (!subdomainData) {\n    return {\n      title: rootDomain\n    };\n  }\n\n  return {\n    title: `${subdomain}.${rootDomain}`,\n    description: `Subdomain page for ${subdomain}.${rootDomain}`\n  };\n}\n\nexport default async function SubdomainPage({\n  params\n}: {\n  params: Promise<{ subdomain: string }>;\n}) {\n  const { subdomain } = await params;\n  const subdomainData = await getSubdomainData(subdomain);\n\n  if (!subdomainData) {\n    notFound();\n  }\n\n  return (\n    <div className=\"flex min-h-screen flex-col bg-gradient-to-b from-blue-50 to-white p-4\">\n      <div className=\"absolute top-4 right-4\">\n        <Link\n          href={`${protocol}://${rootDomain}`}\n          className=\"text-sm text-gray-500 hover:text-gray-700 transition-colors\"\n        >\n          {rootDomain}\n        </Link>\n      </div>\n\n      <div className=\"flex-1 flex items-center justify-center\">\n        <div className=\"text-center\">\n          <div className=\"text-9xl mb-6\">{subdomainData.emoji}</div>\n          <h1 className=\"text-4xl font-bold tracking-tight text-gray-900\">\n            Welcome to {subdomain}.{rootDomain}\n          </h1>\n          <p className=\"mt-3 text-lg text-gray-600\">\n            This is your custom subdomain page\n          </p>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/subdomain-form.tsx",
    "content": "'use client';\n\nimport type React from 'react';\n\nimport { useState } from 'react';\nimport { useActionState } from 'react';\nimport { Button } from '@/components/ui/button';\nimport { Input } from '@/components/ui/input';\nimport { Label } from '@/components/ui/label';\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger\n} from '@/components/ui/popover';\nimport { Smile } from 'lucide-react';\nimport { Card } from '@/components/ui/card';\nimport {\n  EmojiPicker,\n  EmojiPickerContent,\n  EmojiPickerSearch,\n  EmojiPickerFooter\n} from '@/components/ui/emoji-picker';\nimport { createSubdomainAction } from '@/app/actions';\nimport { rootDomain } from '@/lib/utils';\n\ntype CreateState = {\n  error?: string;\n  success?: boolean;\n  subdomain?: string;\n  icon?: string;\n};\n\nfunction SubdomainInput({ defaultValue }: { defaultValue?: string }) {\n  return (\n    <div className=\"space-y-2\">\n      <Label htmlFor=\"subdomain\">Subdomain</Label>\n      <div className=\"flex items-center\">\n        <div className=\"relative flex-1\">\n          <Input\n            id=\"subdomain\"\n            name=\"subdomain\"\n            placeholder=\"your-subdomain\"\n            defaultValue={defaultValue}\n            className=\"w-full rounded-r-none focus:z-10\"\n            required\n          />\n        </div>\n        <span className=\"bg-gray-100 px-3 border border-l-0 border-input rounded-r-md text-gray-500 min-h-[36px] flex items-center\">\n          .{rootDomain}\n        </span>\n      </div>\n    </div>\n  );\n}\n\nfunction IconPicker({\n  icon,\n  setIcon,\n  defaultValue\n}: {\n  icon: string;\n  setIcon: (icon: string) => void;\n  defaultValue?: string;\n}) {\n  const [isPickerOpen, setIsPickerOpen] = useState(false);\n\n  const handleEmojiSelect = ({ emoji }: { emoji: string }) => {\n    setIcon(emoji);\n    setIsPickerOpen(false);\n  };\n\n  return (\n    <div className=\"space-y-2\">\n      <Label htmlFor=\"icon\">Icon</Label>\n      <div className=\"flex flex-col gap-2\">\n        <input type=\"hidden\" name=\"icon\" value={icon} required />\n        <div className=\"flex items-center gap-2\">\n          <Card className=\"flex-1 flex flex-row items-center justify-between p-2 border border-input rounded-md\">\n            <div className=\"min-w-[40px] min-h-[40px] flex items-center pl-[14px] select-none\">\n              {icon ? (\n                <span className=\"text-3xl\">{icon}</span>\n              ) : (\n                <span className=\"text-gray-400 text-sm font-normal\">\n                  No icon selected\n                </span>\n              )}\n            </div>\n            <Popover open={isPickerOpen} onOpenChange={setIsPickerOpen}>\n              <PopoverTrigger asChild>\n                <Button\n                  type=\"button\"\n                  variant=\"outline\"\n                  size=\"sm\"\n                  className=\"ml-auto rounded-sm\"\n                  onClick={() => setIsPickerOpen(!isPickerOpen)}\n                >\n                  <Smile className=\"h-4 w-4 mr-2\" />\n                  Select Emoji\n                </Button>\n              </PopoverTrigger>\n              <PopoverContent\n                className=\"p-0 w-[256px]\"\n                align=\"end\"\n                sideOffset={5}\n              >\n                <EmojiPicker\n                  className=\"h-[300px] w-[256px]\"\n                  defaultValue={defaultValue}\n                  onEmojiSelect={handleEmojiSelect}\n                >\n                  <EmojiPickerSearch />\n                  <EmojiPickerContent />\n                  <EmojiPickerFooter />\n                </EmojiPicker>\n              </PopoverContent>\n            </Popover>\n          </Card>\n        </div>\n        <p className=\"text-xs text-gray-500\">\n          Select an emoji to represent your subdomain\n        </p>\n      </div>\n    </div>\n  );\n}\n\nexport function SubdomainForm() {\n  const [icon, setIcon] = useState('');\n\n  const [state, action, isPending] = useActionState<CreateState, FormData>(\n    createSubdomainAction,\n    {}\n  );\n\n  return (\n    <form action={action} className=\"space-y-4\">\n      <SubdomainInput defaultValue={state?.subdomain} />\n\n      <IconPicker icon={icon} setIcon={setIcon} defaultValue={state?.icon} />\n\n      {state?.error && (\n        <div className=\"text-sm text-red-500\">{state.error}</div>\n      )}\n\n      <Button type=\"submit\" className=\"w-full\" disabled={isPending || !icon}>\n        {isPending ? 'Creating...' : 'Create Subdomain'}\n      </Button>\n    </form>\n  );\n}\n"
  },
  {
    "path": "components/ui/button.tsx",
    "content": "import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst buttonVariants = cva(\n  \"inline-flex items-center justify-center 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 ? 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/dialog.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\"\nimport { XIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Dialog({\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Root>) {\n  return <DialogPrimitive.Root data-slot=\"dialog\" {...props} />\n}\n\nfunction DialogTrigger({\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {\n  return <DialogPrimitive.Trigger data-slot=\"dialog-trigger\" {...props} />\n}\n\nfunction DialogPortal({\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Portal>) {\n  return <DialogPrimitive.Portal data-slot=\"dialog-portal\" {...props} />\n}\n\nfunction DialogClose({\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Close>) {\n  return <DialogPrimitive.Close data-slot=\"dialog-close\" {...props} />\n}\n\nfunction DialogOverlay({\n  className,\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {\n  return (\n    <DialogPrimitive.Overlay\n      data-slot=\"dialog-overlay\"\n      className={cn(\n        \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction DialogContent({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Content>) {\n  return (\n    <DialogPortal data-slot=\"dialog-portal\">\n      <DialogOverlay />\n      <DialogPrimitive.Content\n        data-slot=\"dialog-content\"\n        className={cn(\n          \"bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg\",\n          className\n        )}\n        {...props}\n      >\n        {children}\n        <DialogPrimitive.Close className=\"ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\">\n          <XIcon />\n          <span className=\"sr-only\">Close</span>\n        </DialogPrimitive.Close>\n      </DialogPrimitive.Content>\n    </DialogPortal>\n  )\n}\n\nfunction DialogHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"dialog-header\"\n      className={cn(\"flex flex-col gap-2 text-center sm:text-left\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction DialogFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"dialog-footer\"\n      className={cn(\n        \"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction DialogTitle({\n  className,\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Title>) {\n  return (\n    <DialogPrimitive.Title\n      data-slot=\"dialog-title\"\n      className={cn(\"text-lg leading-none font-semibold\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction DialogDescription({\n  className,\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Description>) {\n  return (\n    <DialogPrimitive.Description\n      data-slot=\"dialog-description\"\n      className={cn(\"text-muted-foreground text-sm\", className)}\n      {...props}\n    />\n  )\n}\n\nexport {\n  Dialog,\n  DialogClose,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogOverlay,\n  DialogPortal,\n  DialogTitle,\n  DialogTrigger,\n}\n"
  },
  {
    "path": "components/ui/emoji-picker.tsx",
    "content": "\"use client\";\n\nimport {\n  type EmojiPickerListCategoryHeaderProps,\n  type EmojiPickerListEmojiProps,\n  type EmojiPickerListRowProps,\n  EmojiPicker as EmojiPickerPrimitive,\n} from \"frimousse\";\nimport { LoaderIcon, SearchIcon } from \"lucide-react\";\nimport type * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction EmojiPicker({\n  className,\n  ...props\n}: React.ComponentProps<typeof EmojiPickerPrimitive.Root>) {\n  return (\n    <EmojiPickerPrimitive.Root\n      className={cn(\n        \"bg-popover text-popover-foreground isolate flex h-full w-fit flex-col overflow-hidden rounded-md\",\n        className\n      )}\n      data-slot=\"emoji-picker\"\n      {...props}\n    />\n  );\n}\n\nfunction EmojiPickerSearch({\n  className,\n  ...props\n}: React.ComponentProps<typeof EmojiPickerPrimitive.Search>) {\n  return (\n    <div\n      className={cn(\"flex h-9 items-center gap-2 border-b px-3\", className)}\n      data-slot=\"emoji-picker-search-wrapper\"\n    >\n      <SearchIcon className=\"size-4 shrink-0 opacity-50\" />\n      <EmojiPickerPrimitive.Search\n        className=\"outline-hidden placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm disabled:cursor-not-allowed disabled:opacity-50\"\n        data-slot=\"emoji-picker-search\"\n        {...props}\n      />\n    </div>\n  );\n}\n\nfunction EmojiPickerRow({ children, ...props }: EmojiPickerListRowProps) {\n  return (\n    <div {...props} className=\"scroll-my-1 px-1\" data-slot=\"emoji-picker-row\">\n      {children}\n    </div>\n  );\n}\n\nfunction EmojiPickerEmoji({\n  emoji,\n  className,\n  ...props\n}: EmojiPickerListEmojiProps) {\n  return (\n    <button\n      {...props}\n      className={cn(\n        \"data-[active]:bg-accent flex size-7 items-center justify-center rounded-sm text-base\",\n        className\n      )}\n      data-slot=\"emoji-picker-emoji\"\n    >\n      {emoji.emoji}\n    </button>\n  );\n}\n\nfunction EmojiPickerCategoryHeader({\n  category,\n  ...props\n}: EmojiPickerListCategoryHeaderProps) {\n  return (\n    <div\n      {...props}\n      className=\"bg-popover text-muted-foreground px-3 pb-2 pt-3.5 text-xs leading-none\"\n      data-slot=\"emoji-picker-category-header\"\n    >\n      {category.label}\n    </div>\n  );\n}\n\nfunction EmojiPickerContent({\n  className,\n  ...props\n}: React.ComponentProps<typeof EmojiPickerPrimitive.Viewport>) {\n  return (\n    <EmojiPickerPrimitive.Viewport\n      className={cn(\"outline-hidden relative flex-1\", className)}\n      data-slot=\"emoji-picker-viewport\"\n      {...props}\n    >\n      <EmojiPickerPrimitive.Loading\n        className=\"absolute inset-0 flex items-center justify-center text-muted-foreground\"\n        data-slot=\"emoji-picker-loading\"\n      >\n        <LoaderIcon className=\"size-4 animate-spin\" />\n      </EmojiPickerPrimitive.Loading>\n      <EmojiPickerPrimitive.Empty\n        className=\"absolute inset-0 flex items-center justify-center text-muted-foreground text-sm\"\n        data-slot=\"emoji-picker-empty\"\n      >\n        No emoji found.\n      </EmojiPickerPrimitive.Empty>\n      <EmojiPickerPrimitive.List\n        className=\"select-none pb-1\"\n        components={{\n          Row: EmojiPickerRow,\n          Emoji: EmojiPickerEmoji,\n          CategoryHeader: EmojiPickerCategoryHeader,\n        }}\n        data-slot=\"emoji-picker-list\"\n      />\n    </EmojiPickerPrimitive.Viewport>\n  );\n}\n\nfunction EmojiPickerFooter({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      className={cn(\n        \"max-w-(--frimousse-viewport-width) flex w-full min-w-0 items-center gap-1 border-t p-2\",\n        className\n      )}\n      data-slot=\"emoji-picker-footer\"\n      {...props}\n    >\n      <EmojiPickerPrimitive.ActiveEmoji>\n        {({ emoji }) =>\n          emoji ? (\n            <>\n              <div className=\"flex size-7 flex-none items-center justify-center text-lg\">\n                {emoji.emoji}\n              </div>\n              <span className=\"text-secondary-foreground truncate text-xs\">\n                {emoji.label}\n              </span>\n            </>\n          ) : (\n            <span className=\"text-muted-foreground ml-1.5 flex h-7 items-center truncate text-xs\">\n              Select an emoji…\n            </span>\n          )\n        }\n      </EmojiPickerPrimitive.ActiveEmoji>\n    </div>\n  );\n}\n\nexport {\n  EmojiPicker,\n  EmojiPickerSearch,\n  EmojiPickerContent,\n  EmojiPickerFooter,\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 * as LabelPrimitive from \"@radix-ui/react-label\"\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/popover.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as PopoverPrimitive from \"@radix-ui/react-popover\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Popover({\n  ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Root>) {\n  return <PopoverPrimitive.Root data-slot=\"popover\" {...props} />\n}\n\nfunction PopoverTrigger({\n  ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {\n  return <PopoverPrimitive.Trigger data-slot=\"popover-trigger\" {...props} />\n}\n\nfunction PopoverContent({\n  className,\n  align = \"center\",\n  sideOffset = 4,\n  ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Content>) {\n  return (\n    <PopoverPrimitive.Portal>\n      <PopoverPrimitive.Content\n        data-slot=\"popover-content\"\n        align={align}\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 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden\",\n          className\n        )}\n        {...props}\n      />\n    </PopoverPrimitive.Portal>\n  )\n}\n\nfunction PopoverAnchor({\n  ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {\n  return <PopoverPrimitive.Anchor data-slot=\"popover-anchor\" {...props} />\n}\n\nexport { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }\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": "lib/redis.ts",
    "content": "import { Redis } from '@upstash/redis';\n\nexport const redis = new Redis({\n  url: process.env.KV_REST_API_URL,\n  token: process.env.KV_REST_API_TOKEN\n});\n"
  },
  {
    "path": "lib/subdomains.ts",
    "content": "import { redis } from '@/lib/redis';\n\nexport function isValidIcon(str: string) {\n  if (str.length > 10) {\n    return false;\n  }\n\n  try {\n    // Primary validation: Check if the string contains at least one emoji character\n    // This regex pattern matches most emoji Unicode ranges\n    const emojiPattern = /[\\p{Emoji}]/u;\n    if (emojiPattern.test(str)) {\n      return true;\n    }\n  } catch (error) {\n    // If the regex fails (e.g., in environments that don't support Unicode property escapes),\n    // fall back to a simpler validation\n    console.warn(\n      'Emoji regex validation failed, using fallback validation',\n      error\n    );\n  }\n\n  // Fallback validation: Check if the string is within a reasonable length\n  // This is less secure but better than no validation\n  return str.length >= 1 && str.length <= 10;\n}\n\ntype SubdomainData = {\n  emoji: string;\n  createdAt: number;\n};\n\nexport async function getSubdomainData(subdomain: string) {\n  const sanitizedSubdomain = subdomain.toLowerCase().replace(/[^a-z0-9-]/g, '');\n  const data = await redis.get<SubdomainData>(\n    `subdomain:${sanitizedSubdomain}`\n  );\n  return data;\n}\n\nexport async function getAllSubdomains() {\n  const keys = await redis.keys('subdomain:*');\n\n  if (!keys.length) {\n    return [];\n  }\n\n  const values = await redis.mget<SubdomainData[]>(...keys);\n\n  return keys.map((key, index) => {\n    const subdomain = key.replace('subdomain:', '');\n    const data = values[index];\n\n    return {\n      subdomain,\n      emoji: data?.emoji || '❓',\n      createdAt: data?.createdAt || Date.now()\n    };\n  });\n}\n"
  },
  {
    "path": "lib/utils.ts",
    "content": "import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport const protocol =\n  process.env.NODE_ENV === 'production' ? 'https' : 'http';\nexport const rootDomain =\n  process.env.NEXT_PUBLIC_ROOT_DOMAIN || 'localhost:3000';\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"
  },
  {
    "path": "middleware.ts",
    "content": "import { type NextRequest, NextResponse } from 'next/server';\nimport { rootDomain } from '@/lib/utils';\n\nfunction extractSubdomain(request: NextRequest): string | null {\n  const url = request.url;\n  const host = request.headers.get('host') || '';\n  const hostname = host.split(':')[0];\n\n  // Local development environment\n  if (url.includes('localhost') || url.includes('127.0.0.1')) {\n    // Try to extract subdomain from the full URL\n    const fullUrlMatch = url.match(/http:\\/\\/([^.]+)\\.localhost/);\n    if (fullUrlMatch && fullUrlMatch[1]) {\n      return fullUrlMatch[1];\n    }\n\n    // Fallback to host header approach\n    if (hostname.includes('.localhost')) {\n      return hostname.split('.')[0];\n    }\n\n    return null;\n  }\n\n  // Production environment\n  const rootDomainFormatted = rootDomain.split(':')[0];\n\n  // Handle preview deployment URLs (tenant---branch-name.vercel.app)\n  if (hostname.includes('---') && hostname.endsWith('.vercel.app')) {\n    const parts = hostname.split('---');\n    return parts.length > 0 ? parts[0] : null;\n  }\n\n  // Regular subdomain detection\n  const isSubdomain =\n    hostname !== rootDomainFormatted &&\n    hostname !== `www.${rootDomainFormatted}` &&\n    hostname.endsWith(`.${rootDomainFormatted}`);\n\n  return isSubdomain ? hostname.replace(`.${rootDomainFormatted}`, '') : null;\n}\n\nexport async function middleware(request: NextRequest) {\n  const { pathname } = request.nextUrl;\n  const subdomain = extractSubdomain(request);\n\n  if (subdomain) {\n    // Block access to admin page from subdomains\n    if (pathname.startsWith('/admin')) {\n      return NextResponse.redirect(new URL('/', request.url));\n    }\n\n    // For the root path on a subdomain, rewrite to the subdomain page\n    if (pathname === '/') {\n      return NextResponse.rewrite(new URL(`/s/${subdomain}`, request.url));\n    }\n  }\n\n  // On the root domain, allow normal access\n  return NextResponse.next();\n}\n\nexport const config = {\n  matcher: [\n    /*\n     * Match all paths except for:\n     * 1. /api routes\n     * 2. /_next (Next.js internals)\n     * 3. all root files inside /public (e.g. /favicon.ico)\n     */\n    '/((?!api|_next|[\\\\w-]+\\\\.\\\\w+).*)'\n  ]\n};\n"
  },
  {
    "path": "next.config.ts",
    "content": "import type { NextConfig } from \"next\";\n\nconst nextConfig: NextConfig = {\n  /* config options here */\n  experimental: {\n    // Enable experimental features if needed\n  },\n  // Ensure proper handling of Vercel Analytics and Speed Insights\n  // headers: async () => {\n  //   return [\n  //     {\n  //       source: '/_vercel/speed-insights/script.js',\n  //       headers: [\n  //         {\n  //           key: 'Cache-Control',\n  //           value: 'public, max-age=31536000, immutable',\n  //         },\n  //       ],\n  //     },\n  //   ];\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  },\n  \"dependencies\": {\n    \"@radix-ui/react-dialog\": \"^1.1.13\",\n    \"@radix-ui/react-label\": \"^2.1.6\",\n    \"@radix-ui/react-popover\": \"^1.1.13\",\n    \"@radix-ui/react-slot\": \"^1.2.2\",\n    \"@upstash/redis\": \"^1.34.9\",\n    \"@vercel/analytics\": \"^1.5.0\",\n    \"@vercel/speed-insights\": \"^1.2.0\",\n    \"class-variance-authority\": \"^0.7.1\",\n    \"clsx\": \"^2.1.1\",\n    \"frimousse\": \"^0.2.0\",\n    \"lucide-react\": \"^0.510.0\",\n    \"next\": \"^15.3.6\",\n    \"react\": \"^19.1.0\",\n    \"react-dom\": \"^19.1.0\",\n    \"tailwind-merge\": \"^3.3.0\"\n  },\n  \"devDependencies\": {\n    \"@tailwindcss/postcss\": \"^4.1.6\",\n    \"@types/node\": \"^22.15.17\",\n    \"@types/react\": \"^19.1.4\",\n    \"@types/react-dom\": \"^19.1.5\",\n    \"tailwindcss\": \"^4.1.6\",\n    \"tw-animate-css\": \"^1.2.9\",\n    \"typescript\": \"^5.8.3\"\n  },\n  \"packageManager\": \"pnpm@10.12.4+sha512.5ea8b0deed94ed68691c9bad4c955492705c5eeb8a87ef86bc62c74a26b037b08ff9570f108b2e4dbd1dd1a9186fea925e527f141c648e85af45631074680184\"\n}\n"
  },
  {
    "path": "postcss.config.mjs",
    "content": "const config = {\n  plugins: [\"@tailwindcss/postcss\"],\n};\n\nexport default config;\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"incremental\": true,\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ],\n    \"paths\": {\n      \"@/*\": [\"./*\"]\n    }\n  },\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".next/types/**/*.ts\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  }
]