[
  {
    "path": ".eslintrc.cjs",
    "content": "/** @type {import(\"eslint\").Linter.Config} */\nconst config = {\n  parser: \"@typescript-eslint/parser\",\n  parserOptions: {\n    project: true,\n  },\n  plugins: [\"@typescript-eslint\"],\n  extends: [\n    \"next/core-web-vitals\",\n    \"plugin:@typescript-eslint/recommended-type-checked\",\n    \"plugin:@typescript-eslint/stylistic-type-checked\",\n  ],\n  rules: {\n    // These opinionated rules are enabled in stylistic-type-checked above.\n    // Feel free to reconfigure them to your own preference.\n    \"@typescript-eslint/array-type\": \"off\",\n    \"@typescript-eslint/consistent-type-definitions\": \"off\",\n\n    \"@typescript-eslint/consistent-type-imports\": [\n      \"warn\",\n      {\n        prefer: \"type-imports\",\n        fixStyle: \"inline-type-imports\",\n      },\n    ],\n    \"@typescript-eslint/no-unused-vars\": [\"warn\", { argsIgnorePattern: \"^_\" }],\n  },\n}\n\nmodule.exports = config\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non: [push, pull_request]\n\nenv:\n  DATABASE_URL: \"https://fake.com\"\n  SKIP_ENV_VALIDATION: true\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v2\n\n      - name: Bun\n        uses: oven-sh/setup-bun@v1\n\n      - name: Install Dependencies\n        run: bun install\n\n      - name: Typecheck\n        run: bun run typecheck\n\n      - name: Lint\n        run: bun run lint\n\n      - name: Print Environment Variable\n        run: echo $MY_ENV_VAR\n"
  },
  {
    "path": ".gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# database\n/prisma/db.sqlite\n/prisma/db.sqlite-journal\n\n# next.js\n/.next/\n/out/\nnext-env.d.ts\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\n\n# local env files\n# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables\n.env\n.env*.local\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\n"
  },
  {
    "path": "README.md",
    "content": "## Project using bleeding-edge stack. Drizzle ORM + Neon postgres + Clerk auth + Shadcn/ui + everything new in Next.js 13 (server components, server actions, streaming ui, parallel routes, intercepting routes). Now fully edge runtime deployed.\n\n### Full Tech Stack\n\n- Next.js 13\n- CreateT3App Bootrapped\n- Neon (postgres)\n- Drizzle ORM\n- Tailwind + Shadcn/ui\n- Clerk\n- Lucide Icons\n- Zod Validation\n- Stripe\n\n### Project Description\n\nNetflix clone, project inspired by [@sadmann17](https://twitter.com/sadmann17). Bootrapped with CreateT3App. Project uses 100% server actions, zero api endpoints aside for webhooks. [next-safe-action](https://github.com/TheEdoRan/next-safe-action) library for typesafe server actions. Each account can have up to 4 profiles. Each profiles have it's own avatar and list of saved shows. Feature includes ability to search show catalog, SaaS subscription service with Stripe, optimistic update, and infinite scrolling.\n\n### How To Run Locally\nClone repo, install dependencies, and set environment variables inside \".env.example\", remember to rename \".env.exmaple\" to \".env\". Run \"npm run dev\" -> \"npm run stripe:listen\"(forward stripe events to local). \n\n### Overall Thoughts\n\nNext.js 13 app router overall was a joy to work with and is GREATLY superior to page router. I will say the Next.js app router docs are currently very terrible and you will be left to figure out many things on your own. However, the tools that Next.js 13 gives you are very powerful and things you won't find in any other framework. Server components are underrated and more powerful than you may think. There is an art to interweaving server and client components that is hard to grasp until you get down and dirty with them. Parrallel and intercepting routes are incredibly useful although very buggy. Lucky for you, I've already figured out most of the bugs/tricky bits so just read the \"Tricky things\" section. Streaming ui and suspense is great. It really makes it easy to handle loading states. This is one of the most impactful things about app dir vs page dir. I saved the most important topic for last. When I first started using server actions I really didn't understand the point. They kind of felt like another way to write api endpoints and just felt like a worst version of tRPC. The Next.js docs will push you to use the server component version of server actions using forms but trust me, don't use server actions with forms. If you do, you are giving up the best feature of server actions which is the tRPC like typesafety. To get the best DX out of server actions, I recommend using [next-safe-actions](https://github.com/TheEdoRan/next-safe-action/tree/main/packages/next-safe-action), this lib is a game changer. It made server actions felt just like tRPC and overall was just an amazing DX. I think it's still too early for server actions to replace tRPC but the nice thing is that it requires zero setup. Setting tRPC up for app dir would be a headache right now. Also note, [revalidatePath/Tag currently only work with server actions](https://github.com/pingdotgg/zact) and you will definitely need them.\n\n### Thoughts about Clerk\n\nClerk was amazing to work with in terms of DX. Extremely easy to setup and get rolling. However, there is a major problems that's a deal breakers until they fix it.\n\n- Clerk causes your entire app to be dyamically rendered. Meaning you can not benefit from things like SSG and ISR. Override with \"cache: force-cache\" or \"revalidate = 0\" is not possible.\n  ![Screenshot (83)](https://github.com/Apestein/nextflix/assets/107362680/6d2d89d0-63f3-4d6c-97a7-3a12f514868e)\n\n### Thoughts about Neon\n\n- Error prone, not production ready, support is slow to respond, and hard to get help because they don't have Discord. Wouldn't recommend for serious projects. Foreign key contraint is nice to have compared to Planetscale. The biggest pro is the [data branching](https://planetscale.com/docs/concepts/data-branching) feature is free. On Planetscale you need \"Scaler Pro\" for this feature. Data branching makes a huge difference for development/debugging. I do miss not having Planetscale's \"Slowest queries during the last 24 hours\" panel in the dashboard.\n\n### Thoughts about Drizzle\n\nFantastic. Noticeably faster than Prisma. Schema file being in typescript results in superior DX. Their docs are a little lacking though.\n\n### Project Setup\n\nCreateT3App comes with some nice things like T3 Env and Typescript-Eslint preconfigured. To bootstrap with CreateT3App, you just need to delete page dir and create app dir. And VERY important, in next.config.mjs you must delete \"i18n\" property.\n\n### Project Structure\n\nSome people like to break everything down into neat little components and organize them into different files. I prefer big files, nothing gets extracted until it gets used in at least 2 different places.\nIf your site is complex you will probably need many different layout. Your root layout.tsx file should contain only the things shared by your entire app. For parts of your site that need different layout use [route groups](https://nextjs.org/docs/app/building-your-application/routing/route-groups).\n![Screenshot (74)](https://github.com/Apestein/nextflix/assets/107362680/44bffd04-e537-49ca-a945-1b1185a4b64f)\n\n### Tricky Things To Consider (I will be going over things I found tricky or difficult in this section)\n\n#### 1. You will no doubt run into problems with Next.js aggressive caching. To invalidate router cache, you must use [RevalidatePath/Tage in server action](https://nextjs.org/docs/app/building-your-application/caching#invalidation-1). However, Revalidate/Tag also causes the current page to refresh. I don't know why Next.js decided to do this but get around this I use this [LinkButton](https://github.com/Apestein/nextflix/blob/main/src/components/link-button.tsx) component. See the problem below.\n\n[scrnli_8_22_2023_12-30-04 PM2.webm](https://github.com/Apestein/nextflix/assets/107362680/da7dd256-0a91-4ce5-99c6-698bc37d8013)\n\n#### 2. When a new user creates an account or signs in with Clerk's oauth I needed to create an account and profile in my database. At first, I was using Clerk's webhook to create them but the problem was users would get redirected to the landing page before the webhook could add the account and profile to the database. As a result, when users first creates the account. The UserButton component that displayed their avatar and profile infomation was missing. To get around this, I had a [CustomUserComponent](<https://github.com/Apestein/nextflix/blob/main/src/app/(main)/layout.tsx>) check if the user exist in the database or not (if not, add).\n\n```ts\nasync function CustomeUserButton() {\n  const { userId } = auth()\n  if (!userId) return\n  const existingAccount = await getAccountWithActiveProfile()\n  const account = existingAccount ?? (await createAccountAndProfile())\n  ...\n}\n```\n\nAdditionally, you should wrap the component in suspense to not block the UI and prevent unresponsiveness.\n\n```ts\n<Suspense fallback={<Skeleton className=\"h-8 w-8\" />}>\n  <CustomeUserButton />\n</Suspense>\n```\n\n#### 3. I had an object that I needed to extract a tuple from to validate with zod. [Here is how](https://github.com/Apestein/nextflix/blob/main/src/lib/configs.ts).\n\n```ts\nexport const createCheckoutSession = authAction(\n  z.object({\n    stripeProductId: z.string(),\n    planName: z.enum(planTuple),\n  }),\n}\n```\n\n![Screenshot (78)](https://github.com/Apestein/nextflix/assets/107362680/98e2f8f8-3b44-46d7-baa6-abe95d8463fa)\n\n#### 4. Infinite scrolling can be tricky to implement yourself. Typically, I would use React Query/SWR to do this but I wanted to implement it with [server actions](https://github.com/Apestein/nextflix/blob/main/src/actions/index.ts) this time.\n\n```ts\n// actions/index.ts\nexport const getMyShowsInfinite = authAction(\n  z.object({\n    index: z.number().min(0),\n    limit: z.number().min(2).max(50),\n  }),\n  async (input) => {\n    const account = await getAccountWithActiveProfile()\n    const shows = await db.query.myShows.findMany({\n      where: eq(myShows.profileId, account.activeProfileId),\n      limit: input.limit + 1,\n      offset: input.index * input.limit,\n    })\n    const hasNextPage = shows.length > input.limit ? true : false\n    if (hasNextPage) shows.pop()\n    const filteredShows = await getMyShowsFromTmdb(shows)\n    return { shows: filteredShows, hasNextPage }\n  },\n)\n```\n\nThen, I use this modified [infinite scroll component](https://github.com/Apestein/better-react-infinite-scroll) that I created. See the implementation [here](<https://github.com/Apestein/nextflix/blob/main/src/app/(main)/my-list/infinite-scroller.tsx>), ignore the stuff about simulated shows. Important thing to understand is inside IntersectionObserver callback function, you must use refs instead of state. That is because of scoping, the callback is only created once and all the variables inside are snapshotted. To get around this you need to use refs. There maybe other ways, I'm just listing what I know.\n\n```ts\nconst observer = new IntersectionObserver((entries) => {\n  if (!hasNextPageRef.current) return // <= must use ref, don't use state\n})\n```\n\n[scrnli_8_22_2023_12-42-31 PM3.webm](https://github.com/Apestein/nextflix/assets/107362680/e9ceae54-1ea0-4c89-97c7-0d87d12bd135)\n\n#### 5. For Stripe intergration. Reference these 2 repos and mine also of course. Be careful with webhooks, use the Stripe CLI to forward events to your local environment when testing.\n\n- [Official Next.js example using server actions](https://github.com/vercel/next.js/tree/canary/examples/with-stripe-typescript)\n- [Taxonomy](https://github.com/shadcn-ui/taxonomy)\n\n#### 6. Optimistic update with server actions can be tricky. Using next-safe-action's useOptimisticAction hook helps here. [Here is how I did it](https://github.com/Apestein/nextflix/blob/main/src/components/modal-card.tsx).\n\n[scrnli_8_22_2023_12-17-36 PM.webm](https://github.com/Apestein/nextflix/assets/107362680/00f9690a-8698-498a-b639-5e45b5e5518c)\n\n#### 7. To prevent the search function from firing with every keystroke. Use the [use-debounce package](https://www.npmjs.com/package/use-debounce). [See my implementation here](<https://github.com/Apestein/nextflix/blob/main/src/app/(main)/search/search-input.tsx>). All data fetching can be done with server component by using router.push()/replace(). Pretty crazy pattern if you ask me🤯.\n\n[scrnli_8_22_2023_12-51-37 PM4.webm](https://github.com/Apestein/nextflix/assets/107362680/3dda2e70-97f5-4d88-beca-1cfda53fc344)\n\n#### 8. Very frustrating problem I ran into was the scrollbar causing layout shift. When users navigate from a page with scrollbar to a page without scrollbar there would be an annoying layout shift.\n\nhttps://github.com/Apestein/nextflix/assets/107362680/4136a245-e38f-404a-b66e-2a9c4bc1b266\n\nThe solution is to use [scrollbar-gutter css property](https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-gutter). However, the documentation is terrible and I couldn't get it to work after many hours of debugging. I ended up just googling \"scrollbar-gutter not working\" and found this [stackoverflow answer](https://stackoverflow.com/questions/75732399/why-doesnt-scrollbar-gutter-stable-work-on-the-body-element) lol. Basically, I just need to place scrollbar-gutter:stable on the [HTML element](https://github.com/Apestein/nextflix/blob/main/src/app/layout.tsx)🤦‍♂️. However this introduced a new bug when using \"scrollbar-gutter\" with modals, specifically Shadcn/ui & Radix modals. Opening a modal causes layout shift.\n\n[scrnli_8_25_2023_5-58-40 PM5.webm](https://github.com/Apestein/nextflix/assets/107362680/9e5e7f93-8d1a-493f-8eba-c6dc2b283805)\n\nMoreover, since I must set \"scrollbar-gutter\" on the HTML element. This meant that my whole application will have a gutter (small padding on the right) on every page, we wouldn't want a gutter on our auth pages for example. After some research, here is the solution that I came up with:\n\n```ts\n//app/layout.tsx\n<html\n  lang=\"en\"\n  suppressHydrationWarning\n  className=\"[&:not(:has([role='dialog'])):has([data-layout='main'])]:[scrollbar-gutter:stable]\"\n  //Basically, this means only set \"scrollbar-gutter:stable\" when current page has both element with attribute NOT [role=\"dialog\"] (ie. when modal is NOT open) and element with atrribute [data-layout=main] (ie. when we are in page of \"(main)\" route group). Wow, who knew CSS could be so powerful🤯\n>\n  ...\n</html>\n```\n\n```ts\n//app/(main)/layout.tsx\n<div\n  className=\"container flex min-h-screen flex-col px-4 md:px-8\"\n  data-layout=\"main\" //<= add this\n>\n  <Header />\n  {children}\n  <Footer />\n</div>\n```\n\nEdit: Unfortunately, the solution above introduced some new layout shift. But I think the solution is cool so I will leave it here. This is the new solution, I just force a scrollbar on the (main) route group. Not the perfect solution but it's the best I can up come with.\n\n```ts\n//app/layout.tsx\n<body\n  className={cn(\n    \"bg-neutral-900 text-slate-50 antialiased [&:has([data-layout='main'])]:overflow-y-scroll\",\n    inter.className,\n  )}\n>\n  ...\n</body>\n```\nEdit: Now I'm using [OverlayScrollbars](https://kingsora.github.io/OverlayScrollbars/). I think this is the best solution. See my implementation [here](https://github.com/Apestein/nextflix/blob/main/src/components/overlay-scrollbar.tsx).\n\n#### 9. For modal using [intercepting route](https://nextjs.org/docs/app/building-your-application/routing/intercepting-routes#modals). Follow next.js [official example(https://github.com/vercel-labs/nextgram). You can only use router.back() to close the modal as far as I know. By default when opening the intercepting modal, it will cause page to scroll either all the way up or down. To prevent this, set scroll={false} on Link.\n```ts\n<Link\n  href={`/show/${show.id}?mediaType=${\n  show.title ? \"movie\" : \"tv\"\n  }`}\n  scroll={false}\n  key={show.id}\n>\n```\nIf you have a loading.tsx file for the modal it should go in the @modal folder [like this](<https://github.com/Apestein/nextflix/tree/main/src/app/(main)/%40modal>). You can see just how much of different intercepting modal makes vs normal modal by looking at my previous solution. [Previous](https://github.com/Apestein/nextflix/blob/normal-modal/src/components/show-card.tsx) vs [New](<https://github.com/Apestein/nextflix/blob/main/src/app/(main)/%40modal/(.)show/%5Bid%5D/page.tsx>)\n\n#### 10. To deploy to the edge, you only need 2 lines of code. Since my database is located in US East, edge can be [slower than normal serverless lambda](https://vercel.com/docs/functions/edge-functions#using-a-database-with-edge-functions) if I don't set a preferredRegion close to my database location. Currently, there is bug with Clerk and Next.js in local development if you're on Windows. Just comment out the edge runtime export when in development, when you deploy to vercel it should be fine.\n\n```ts\nexport const runtime = \"edge\"\nexport const preferredRegion = \"iad1\"\n```\n\n#### 11. Next.js image component is more complicated than you think, check out this [video](https://www.youtube.com/watch?v=gpJKj45AikY). To optimize, first I request the smallest resolution necessary. TMDB api won't let us request anything smaller than 300w, but ideally it should be 240w since I know that's the maximum size it can be. You should use Next.js Image component here, I can't with my app because hobby plan has limits on image optimization. \n```ts\n<img\n  src={`https://image.tmdb.org/t/p/w300${\n  show.backdrop_path ?? show.poster_path\n}`}\n  alt=\"show-backdrop\"\n  width={240} //should be on-screen rendered size not image actual resolution\n  height={135} //should be on-screen rendered size not image actual resolution\n  className=\"aspect-video min-w-[160px] cursor-pointer object-cover transition-transform hover:scale-110 md:min-w-[240px]\"\n  />\n```\n\n#### 12. You should move your linting and typechecking to Github workflows instead of Vercel, this will [greatly reduce build times](https://youtu.be/YkOSUVzOAA4?t=10047). It's very easy, just include this [file](https://github.com/Apestein/nextflix/blob/7170c65c9928bbaf296196bdc54fd4e43e64a1bb/.github/workflows/ci.yml) in .github/workflows folder. \"DATABASE_URL\" can be any valid URL. If using T3 Env or bootrapping with CreateT3App set \"SKIP_ENV_VALIDATION: true\" to skip env check on Github.\n\n#### 13. Carousel is created using [react-snap-carousel](https://github.com/richardscarrott/react-snap-carousel). Carousel drag scroll is implement using [react-use-draggable-scroll](https://github.com/rfmiotto/react-use-draggable-scroll). See my implementation [here](https://github.com/Apestein/nextflix/blob/main/src/components/show-carousel.tsx). \n\n### Follow and ask me questions at [@Apestein_Dev](https://twitter.com/Apestein_Dev).\n"
  },
  {
    "path": "components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"default\",\n  \"rsc\": true,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"config\": \"tailwind.config.js\",\n    \"css\": \"src/styles/globals.css\",\n    \"baseColor\": \"neutral\",\n    \"cssVariables\": true\n  },\n  \"aliases\": {\n    \"components\": \"~/components\",\n    \"utils\": \"~/lib/utils\"\n  }\n}"
  },
  {
    "path": "drizzle.config.ts",
    "content": "import type { Config } from \"drizzle-kit\"\nimport \"dotenv/config\"\n\nexport default {\n  schema: \"./src/db/schema.ts\",\n  out: \"./drizzle\",\n  driver: \"pg\",\n  dbCredentials: {\n    connectionString: process.env.DATABASE_URL!,\n  },\n  schemaFilter: [\"public\"],\n  verbose: true,\n  strict: true,\n} satisfies Config\n"
  },
  {
    "path": "next.config.mjs",
    "content": "/**\n * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful\n * for Docker builds.\n */\nawait import(\"./src/env.mjs\")\n\n/** @type {import(\"next\").NextConfig} */\nconst config = {\n  images: {\n    domains: [\"image.tmdb.org\", \"img.clerk.com\", \"api.dicebear.com\"],\n  },\n  experimental: {\n    serverActions: true,\n  },\n  typescript: {\n    ignoreBuildErrors: true,\n  },\n  eslint: {\n    ignoreDuringBuilds: true,\n  },\n}\n\nexport default config\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"nextflix\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"build\": \"next build\",\n    \"dev\": \"next dev\",\n    \"lint\": \"next lint\",\n    \"typecheck\": \"tsc\",\n    \"start\": \"next start\",\n    \"introspect\": \"drizzle-kit introspect:pg\",\n    \"generate\": \"drizzle-kit generate:pg\",\n    \"migrate\": \"tsx ./src/db/migrate.ts\",\n    \"push\": \"drizzle-kit push:pg\",\n    \"studio\": \"drizzle-kit studio\",\n    \"stripe:listen\": \"stripe listen --forward-to localhost:3000/api/stripe\"\n  },\n  \"dependencies\": {\n    \"@clerk/nextjs\": \"^4.23.3\",\n    \"@neondatabase/serverless\": \"^0.6.0\",\n    \"@radix-ui/react-dropdown-menu\": \"^2.0.5\",\n    \"@radix-ui/react-label\": \"^2.0.2\",\n    \"@radix-ui/react-scroll-area\": \"^1.0.4\",\n    \"@radix-ui/react-separator\": \"^1.0.3\",\n    \"@radix-ui/react-slot\": \"^1.0.2\",\n    \"@radix-ui/react-toast\": \"^1.1.4\",\n    \"@t3-oss/env-nextjs\": \"^0.6.1\",\n    \"@vercel/analytics\": \"^1.0.2\",\n    \"class-variance-authority\": \"^0.7.0\",\n    \"clsx\": \"^2.0.0\",\n    \"dotenv\": \"^16.3.1\",\n    \"drizzle-orm\": \"^0.28.5\",\n    \"lucide-react\": \"^0.274.0\",\n    \"next\": \"^13.4.19\",\n    \"next-safe-action\": \"^3.0.1\",\n    \"next-themes\": \"^0.2.1\",\n    \"overlayscrollbars-react\": \"^0.5.2\",\n    \"pg\": \"^8.11.3\",\n    \"postgres\": \"^3.3.5\",\n    \"react\": \"18.2.0\",\n    \"react-dom\": \"18.2.0\",\n    \"react-snap-carousel\": \"^0.3.2\",\n    \"react-use-draggable-scroll\": \"^0.4.7\",\n    \"stripe\": \"^13.4.0\",\n    \"tailwind-merge\": \"^1.14.0\",\n    \"tailwindcss-animate\": \"^1.0.7\",\n    \"use-debounce\": \"^9.0.4\",\n    \"zod\": \"^3.22.2\",\n    \"zod-validation-error\": \"^1.5.0\"\n  },\n  \"devDependencies\": {\n    \"@types/eslint\": \"^8.44.2\",\n    \"@types/node\": \"^20.5.9\",\n    \"@types/prettier\": \"^2.7.3\",\n    \"@types/react\": \"^18.2.21\",\n    \"@types/react-dom\": \"^18.2.7\",\n    \"@typescript-eslint/eslint-plugin\": \"^6.5.0\",\n    \"@typescript-eslint/parser\": \"^6.5.0\",\n    \"autoprefixer\": \"^10.4.15\",\n    \"drizzle-kit\": \"^0.19.13\",\n    \"eslint\": \"^8.48.0\",\n    \"eslint-config-next\": \"^13.4.19\",\n    \"postcss\": \"^8.4.29\",\n    \"prettier\": \"^3.0.3\",\n    \"prettier-plugin-tailwindcss\": \"^0.5.4\",\n    \"tailwind-scrollbar\": \"^3.0.5\",\n    \"tailwindcss\": \"^3.3.3\",\n    \"tsx\": \"^3.12.8\",\n    \"typescript\": \"^5.2.2\"\n  }\n}\n"
  },
  {
    "path": "postcss.config.cjs",
    "content": "const config = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n};\n\nmodule.exports = config;\n"
  },
  {
    "path": "prettier.config.cjs",
    "content": "/** @type {import(\"prettier\").Config} */\nconst config = {\n  plugins: [require.resolve(\"prettier-plugin-tailwindcss\")],\n  semi: false,\n};\n\nmodule.exports = config;\n"
  },
  {
    "path": "src/actions/index.ts",
    "content": "\"use server\"\nimport { z } from \"zod\"\nimport { authAction } from \"./safe-action-client\"\nimport { db } from \"~/db/client\"\nimport { eq } from \"drizzle-orm\"\nimport { accounts, profiles, myShows } from \"~/db/schema\"\nimport { ERR } from \"~/lib/utils\"\nimport { revalidatePath } from \"next/cache\"\nimport {\n  getAccount,\n  getAccountWithProfiles,\n  getProfile,\n  getAccountWithActiveProfile,\n  getMyShowsFromTmdb,\n} from \"~/lib/server-fetchers\"\nimport { stripe } from \"~/lib/stripe\"\nimport { headers } from \"next/headers\"\nimport { redirect } from \"next/navigation\"\nimport type { Stripe } from \"stripe\"\nimport { planTuple } from \"~/lib/configs\"\nimport { MediaTuple } from \"~/lib/types\"\n\nexport const createProfile = authAction(\n  z.object({\n    name: z.string().min(2).max(20),\n  }),\n  async (input, { userId }) => {\n    const account = await getAccountWithProfiles()\n    if (account.profiles.length === 4) throw new Error(ERR.not_allowed)\n    const takenProfileSlots = account.profiles.map((profile) =>\n      Number(profile.id.at(-1)),\n    )\n    const openProfileSlot = [1, 2, 3, 4].find(\n      (el) => !takenProfileSlots.includes(el),\n    )\n    if (!openProfileSlot) throw new Error(ERR.undefined)\n    await db.insert(profiles).values({\n      id: `${userId}-${openProfileSlot}`,\n      accountId: userId,\n      name: input.name,\n      profileImgPath: `https://api.dicebear.com/6.x/bottts-neutral/svg?seed=${input.name}`,\n    })\n    revalidatePath(\"/manage-profile\")\n    return { message: \"Profile Created\" }\n  },\n)\n\nexport const deleteProfile = authAction(\n  z.object({\n    profileId: z.string(),\n  }),\n  async (input) => {\n    const account = await getAccountWithProfiles()\n    if (account.activeProfileId === input.profileId)\n      return { message: \"Cannot delete active profile\" }\n    if (!account.profiles.find((profile) => profile.id === input.profileId))\n      throw new Error(ERR.unauthorized)\n    await db.delete(profiles).where(eq(profiles.id, input.profileId))\n    revalidatePath(\"/manage-profile\")\n    return { message: \"Profile Deleted\" }\n  },\n)\n\nexport const updateProfile = authAction(\n  z.object({\n    profileId: z.string(),\n    name: z.string().min(2).max(20),\n  }),\n  async (input, { userId }) => {\n    const profile = await getProfile(input.profileId)\n    if (userId !== profile.accountId) throw new Error(ERR.unauthorized)\n    await db\n      .update(profiles)\n      .set({\n        name: input.name,\n        profileImgPath: `https://api.dicebear.com/6.x/bottts-neutral/svg?seed=${input.name}`,\n      })\n      .where(eq(profiles.id, input.profileId))\n    revalidatePath(\"/manage-profile\")\n    return { message: \"Profile Updated\" }\n  },\n)\n\nexport const switchProfile = authAction(\n  z.object({\n    profileId: z.string(),\n  }),\n  async (input, { userId }) => {\n    const profile = await getProfile(input.profileId)\n    if (profile.accountId !== userId) throw new Error(ERR.unauthorized)\n    await db\n      .update(accounts)\n      .set({\n        activeProfileId: input.profileId,\n      })\n      .where(eq(accounts.id, userId))\n    revalidatePath(\"/\")\n    return { message: \"You have switched active profile\" }\n  },\n)\n\nexport const toggleMyShow = authAction(\n  z.object({\n    id: z.number(),\n    isSaved: z.boolean(),\n    movieOrTv: z.enum(MediaTuple),\n  }),\n  async (input) => {\n    const account = await getAccount()\n    if (!input.isSaved) {\n      await db.insert(myShows).values({\n        id: input.id,\n        mediaType: input.movieOrTv,\n        profileId: account.activeProfileId,\n      })\n      return { isSaved: true }\n    } else {\n      await db.delete(myShows).where(eq(myShows.id, input.id))\n      return { isSaved: false }\n    }\n  },\n)\n\nexport const createCheckoutSession = authAction(\n  z.object({\n    stripeProductId: z.string(),\n    planName: z.enum(planTuple),\n  }),\n  async (input, { userId }) => {\n    const account = await getAccount()\n    const siteUrl = headers().get(\"origin\")!\n    let checkoutSession: Stripe.Checkout.Session | Stripe.BillingPortal.Session\n    if (input.planName !== \"free\" && account.membership === \"free\")\n      checkoutSession = await stripe.checkout.sessions.create({\n        mode: \"subscription\",\n        billing_address_collection: \"auto\",\n        customer_email: account.email,\n        line_items: [\n          {\n            price: input.stripeProductId,\n            quantity: 1,\n          },\n        ],\n        success_url: `${siteUrl}/subscription/result?session_id={CHECKOUT_SESSION_ID}`,\n        cancel_url: `${siteUrl}/subscription`,\n        metadata: {\n          userId,\n          planName: input.planName,\n        },\n      })\n    else\n      checkoutSession = await stripe.billingPortal.sessions.create({\n        customer: account.stripeCustomerId!,\n        return_url: `${siteUrl}/subscription`,\n      })\n    redirect(checkoutSession.url!)\n  },\n)\n\nexport const getMyShowsInfinite = authAction(\n  z.object({\n    index: z.number().min(0),\n    limit: z.number().min(2).max(50),\n  }),\n  async (input) => {\n    const account = await getAccountWithActiveProfile()\n    const shows = await db.query.myShows.findMany({\n      where: eq(myShows.profileId, account.activeProfileId),\n      limit: input.limit + 1,\n      offset: input.index * input.limit,\n    })\n    const hasNextPage = shows.length > input.limit ? true : false\n    if (hasNextPage) shows.pop()\n    const filteredShows = await getMyShowsFromTmdb(shows)\n    return { shows: filteredShows, hasNextPage }\n  },\n)\n"
  },
  {
    "path": "src/actions/safe-action-client.ts",
    "content": "/* eslint-disable @typescript-eslint/require-await */\nimport { createSafeActionClient } from \"next-safe-action\"\nimport { auth } from \"@clerk/nextjs\"\nimport { ERR } from \"~/lib/utils\"\n\nexport const action = createSafeActionClient()\n\nexport const authAction = createSafeActionClient({\n  buildContext: async () => {\n    const userId = auth().userId\n    if (!userId) throw new Error(ERR.unauthenticated)\n    return {\n      userId,\n    }\n  },\n})\n"
  },
  {
    "path": "src/app/(auth)/layout.tsx",
    "content": "export default function AuthLayout({\n  children,\n}: {\n  children: React.ReactNode\n}) {\n  return (\n    <div className=\"grid min-h-screen place-content-center bg-slate-50\">\n      {children}\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/app/(auth)/sign-in/[[...sign-in]]/page.tsx",
    "content": "import { SignIn } from \"@clerk/nextjs\"\n\nexport default function Page() {\n  return <SignIn />\n}\n"
  },
  {
    "path": "src/app/(auth)/sign-up/[[...sign-up]]/page.tsx",
    "content": "import { SignUp } from \"@clerk/nextjs\"\n\nexport default function Page() {\n  return <SignUp />\n}\n"
  },
  {
    "path": "src/app/(main)/@modal/(.)show/[id]/modal.tsx",
    "content": "\"use client\"\nimport type { ShowWithVideoAndGenre } from \"~/lib/types\"\nimport { useRef, useEffect } from \"react\"\nimport { useRouter } from \"next/navigation\"\nimport { ModalCard } from \"~/components/modal-card\"\n\nexport function Modal({\n  show,\n  isSaved,\n}: {\n  show: ShowWithVideoAndGenre\n  isSaved?: boolean\n}) {\n  const overlay = useRef(null)\n  const router = useRouter()\n\n  useEffect(() => {\n    const back = (e: KeyboardEvent) => e.key === \"Escape\" && router.back()\n    document.addEventListener(\"keydown\", back)\n    return () => document.removeEventListener(\"keydown\", back)\n  }, [])\n\n  return (\n    <div\n      ref={overlay}\n      onClick={(e) => e.target === overlay.current && router.back()}\n      className=\"fixed inset-0 bg-black/60\"\n      id=\"show-modal\"\n    >\n      <ModalCard\n        show={show}\n        isSaved={isSaved}\n        className=\"absolute left-1/2 top-1/2 w-full max-w-3xl -translate-x-1/2 -translate-y-1/2\"\n      />\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/app/(main)/@modal/(.)show/[id]/page.tsx",
    "content": "import { Modal } from \"./modal\"\nimport type { MediaType } from \"~/lib/types\"\nimport { getShowVideoAndGenreWithStatus } from \"~/lib/server-fetchers\"\n\nexport default async function ShowModal(props: {\n  params: { id: number }\n  searchParams: { mediaType: MediaType }\n}) {\n  const { show, isSaved } = await getShowVideoAndGenreWithStatus(\n    props.params.id,\n    props.searchParams.mediaType,\n  )\n\n  return <Modal show={show} isSaved={isSaved} />\n}\n"
  },
  {
    "path": "src/app/(main)/@modal/default.tsx",
    "content": "export default function Default() {\n  return null\n}\n"
  },
  {
    "path": "src/app/(main)/@modal/loading.tsx",
    "content": "import { Skeleton } from \"~/components/ui/skeleton\"\n\nexport default function Loading() {\n  return (\n    <div className=\"fixed inset-0 bg-black/60\">\n      <Skeleton className=\"absolute left-1/2 top-1/2 h-[50vh] w-full max-w-3xl -translate-x-1/2 -translate-y-1/2 rounded-lg\" />\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/app/(main)/account/loading.tsx",
    "content": "import { Skeleton } from \"~/components/ui/skeleton\"\n\nexport default function Loading() {\n  return (\n    <main className=\"my-12 flex justify-center\">\n      <Skeleton className=\"h-[713px] w-[500px]\" />\n    </main>\n  )\n}\n"
  },
  {
    "path": "src/app/(main)/account/page.tsx",
    "content": "import { CreditCard, ChevronRight } from \"lucide-react\"\nimport { Button } from \"~/components/ui/button\"\nimport Link from \"next/link\"\nimport { getAccountWithProfiles } from \"~/lib/server-fetchers\"\n\nexport default async function AccountPage() {\n  const account = await getAccountWithProfiles()\n  return (\n    <main className=\"mt-[2.5%] flex justify-center \">\n      <div className=\"space-y-5 md:w-[500px]\">\n        <section className=\"space-y-2\">\n          <h1 className=\"text-4xl\">Account</h1>\n          <p className=\"flex items-center gap-2 text-sm text-neutral-400\">\n            <CreditCard />\n            Member Since: {account.createdAt.toDateString()}\n          </p>\n        </section>\n        <div aria-label=\"divider\" className=\"h-px w-full bg-white/25\" />\n        <p className=\"text-2xl text-neutral-400\">MEMBERSHIP & BILLING</p>\n        <p className=\"flex cursor-pointer justify-between\">\n          {account.email}\n          <ChevronRight />\n        </p>\n        <div aria-label=\"divider\" className=\"h-px w-full bg-white/25\" />\n        <p className=\"flex cursor-pointer justify-between\">\n          Update Account\n          <ChevronRight />\n        </p>\n        <div aria-label=\"divider\" className=\"h-px w-full bg-white/25\" />\n        <Button className=\"w-full\" asChild>\n          <Link href=\"/subscription\">Manage Subscription</Link>\n        </Button>\n        <div aria-label=\"divider\" className=\"h-px w-full bg-white/25\" />\n        <p className=\"text-2xl text-neutral-400\">Plan Details</p>\n        <p className=\"flex gap-1.5\">\n          {`${account.membership\n            .charAt(0)\n            .toUpperCase()}${account.membership.substring(1)}`}\n          <span className=\"rounded-sm px-1 text-neutral-100 ring-2 ring-slate-100\">\n            4K+HDR\n          </span>\n        </p>\n        <div aria-label=\"divider\" className=\"h-px w-full bg-white/25\" />\n        <p className=\"flex cursor-pointer justify-between\">\n          Change plan\n          <ChevronRight />\n        </p>\n        <div aria-label=\"divider\" className=\"h-px w-full bg-white/25\" />\n        <p className=\"text-2xl text-neutral-400\">Profiles</p>\n        <div className=\"flex gap-4 md:gap-8\">\n          {account.profiles.map((profile) => (\n            <div key={profile.id} className=\"space-y-1.5\">\n              {/* eslint-disable-next-line @next/next/no-img-element */}\n              <img\n                src={profile.profileImgPath}\n                alt=\"profile-img\"\n                className=\"w-14 rounded-lg md:w-24\"\n              />\n              <h3 className=\"text-center text-sm md:text-base\">\n                {profile.name}\n              </h3>\n            </div>\n          ))}\n        </div>\n        <div aria-label=\"divider\" className=\"h-px w-full bg-white/25\" />\n      </div>\n    </main>\n  )\n}\n"
  },
  {
    "path": "src/app/(main)/default.tsx",
    "content": "export default function Default() {\n  return null\n}\n"
  },
  {
    "path": "src/app/(main)/layout.tsx",
    "content": "import Image from \"next/image\"\nimport Link from \"next/link\"\nimport { currentUser, SignedOut, auth, SignOutButton } from \"@clerk/nextjs\"\nimport { Suspense } from \"react\"\nimport { Button } from \"~/components/ui/button\"\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from \"~/components/ui/dropdown-menu\"\nimport { Skeleton } from \"~/components/ui/skeleton\"\nimport { db } from \"~/db/client\"\nimport { accounts, profiles } from \"~/db/schema\"\nimport { eq } from \"drizzle-orm\"\nimport { ERR } from \"~/lib/utils\"\nimport {\n  Search,\n  Bell,\n  Facebook,\n  Instagram,\n  Twitter,\n  Youtube,\n  Home,\n  Clapperboard,\n  Film,\n  TrendingUp,\n  List,\n  Pencil,\n  ArrowLeftRight,\n  User,\n  BadgeCheck,\n} from \"lucide-react\"\nimport { LinkButton } from \"~/components/link-button\"\nimport { getAccountWithActiveProfile } from \"~/lib/server-fetchers\"\nimport { OverlayScrollbar } from \"~/components/overlay-scrollbar\"\n\nexport default function ShowsLayout({\n  children,\n  modal,\n}: {\n  children: React.ReactNode\n  modal: React.ReactNode\n}) {\n  return (\n    <div className=\"container flex min-h-screen flex-col px-4 md:px-8\">\n      <Header />\n      {children}\n      {modal}\n      <Footer />\n      <OverlayScrollbar />\n    </div>\n  )\n}\n\nconst NAVINFO = [\n  { name: \"Home\", href: \"/\", icon: <Home className=\"w-5\" /> },\n  {\n    name: \"TV Shows\",\n    href: \"/tv-shows\",\n    icon: <Clapperboard className=\"w-5\" />,\n  },\n  { name: \"Movies\", href: \"/movies\", icon: <Film className=\"w-5\" /> },\n  {\n    name: \"New & Popular\",\n    href: \"/new-and-popular\",\n    icon: <TrendingUp className=\"w-5\" />,\n  },\n  { name: \"My List\", href: \"/my-list\", icon: <List className=\"w-5\" /> },\n]\n\nfunction Header() {\n  return (\n    <header className=\"flex h-16 justify-between\">\n      <div className=\"flex items-center gap-12\">\n        <Link href=\"/\" className=\"hidden md:block\">\n          <Image\n            src=\"/netflix-logo.svg\"\n            alt=\"netflix-logo\"\n            width={300}\n            height={81}\n            priority\n            className=\"h-auto w-28 transition-opacity hover:opacity-80 active:opacity-100\"\n          />\n        </Link>\n        <MainMenu />\n        <nav className=\"hidden gap-6 text-sm md:flex\">\n          {NAVINFO.map((el) =>\n            el.name === \"My List\" ? (\n              <LinkButton href={el.href} key={el.name}>\n                {el.name}\n              </LinkButton>\n            ) : (\n              <Link href={el.href} key={el.name}>\n                {el.name}\n              </Link>\n            ),\n          )}\n        </nav>\n      </div>\n      <div className=\"flex items-center gap-6\">\n        <Link href=\"/search?keyword=\" aria-label=\"search\">\n          <Search />\n        </Link>\n        <Bell />\n        <Suspense fallback={<Skeleton className=\"h-8 w-8\" />}>\n          <CustomeUserButton />\n        </Suspense>\n        <SignedOut>\n          <Button\n            asChild\n            className=\"bg-red-600 font-semibold text-white hover:bg-red-700 active:bg-red-800\"\n          >\n            <Link href=\"/sign-in\">Sign In</Link>\n          </Button>\n        </SignedOut>\n      </div>\n    </header>\n  )\n}\n\nasync function CustomeUserButton() {\n  const { userId } = auth()\n  if (!userId) return\n  const existingAccount = await db.query.accounts.findFirst({\n    where: eq(accounts.id, userId),\n    with: { activeProfile: true },\n  })\n  const account = existingAccount ?? (await createAccountAndProfile())\n  return (\n    <DropdownMenu modal={false}>\n      <DropdownMenuTrigger>\n        {/* eslint-disable-next-line @next/next/no-img-element */}\n        <img\n          src={account.activeProfile.profileImgPath}\n          alt=\"user-image\"\n          height=\"32\"\n          width=\"32\"\n          className=\"rounded-sm\"\n        />\n      </DropdownMenuTrigger>\n      <DropdownMenuContent>\n        <DropdownMenuLabel>{account.activeProfile.name}</DropdownMenuLabel>\n        <DropdownMenuSeparator />\n        <Link href=\"/manage-profile\">\n          <DropdownMenuItem className=\"gap-1.5\">\n            <Pencil className=\"w-5\" />\n            Manage Profile\n          </DropdownMenuItem>\n        </Link>\n        <Link href=\"/switch-profile\">\n          <DropdownMenuItem className=\"gap-1.5\">\n            <ArrowLeftRight className=\"w-5\" />\n            Switch Profile\n          </DropdownMenuItem>\n        </Link>\n        <Link href=\"/account\">\n          <DropdownMenuItem className=\"gap-1.5\">\n            <User className=\"w-5\" />\n            Account\n          </DropdownMenuItem>\n        </Link>\n        <Link href=\"/subscription\">\n          <DropdownMenuItem className=\"gap-1.5\">\n            <BadgeCheck className=\"w-5\" />\n            Subscription\n          </DropdownMenuItem>\n        </Link>\n        <DropdownMenuItem>\n          <SignOutButton>\n            <Button className=\"w-full font-semibold\">Sign Out</Button>\n          </SignOutButton>\n        </DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  )\n}\n\nfunction MainMenu() {\n  return (\n    <DropdownMenu modal={false}>\n      <DropdownMenuTrigger className=\"flex items-center gap-1.5 md:hidden\">\n        <svg viewBox=\"0 0 24 24\" className=\"h-5 w-5 text-red-600\">\n          <path\n            fill=\"currentColor\"\n            d=\"M5.398 0v.006c3.028 8.556 5.37 15.175 8.348 23.596 2.344.058 4.85.398 4.854.398-2.8-7.924-5.923-16.747-8.487-24zm8.489 0v9.63L18.6 22.951c-.043-7.86-.004-15.913.002-22.95zM5.398 1.05V24c1.873-.225 2.81-.312 4.715-.398v-9.22z\"\n          ></path>\n        </svg>\n        <h2 className=\"font-semibold\">Menu</h2>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent>\n        <DropdownMenuLabel className=\"flex gap-1.5\">\n          <svg viewBox=\"0 0 24 24\" className=\"w-5 text-red-600\">\n            <path\n              fill=\"currentColor\"\n              d=\"M5.398 0v.006c3.028 8.556 5.37 15.175 8.348 23.596 2.344.058 4.85.398 4.854.398-2.8-7.924-5.923-16.747-8.487-24zm8.489 0v9.63L18.6 22.951c-.043-7.86-.004-15.913.002-22.95zM5.398 1.05V24c1.873-.225 2.81-.312 4.715-.398v-9.22z\"\n            ></path>\n          </svg>\n          Netflix\n        </DropdownMenuLabel>\n        <DropdownMenuSeparator />\n        {NAVINFO.map((el) => (\n          <Link href={el.href} key={el.name}>\n            <DropdownMenuItem className=\"gap-1.5\">\n              {el.icon}\n              {el.name}\n            </DropdownMenuItem>\n          </Link>\n        ))}\n      </DropdownMenuContent>\n    </DropdownMenu>\n  )\n}\n\nfunction Footer() {\n  return (\n    <footer className=\"mt-auto pb-3 pt-12 text-sm\">\n      <i className=\"flex gap-3 py-3\">\n        <Link href=\"/\" aria-label=\"facebook\">\n          <Facebook className=\"hover:text-red-500\" />\n        </Link>\n        <Link href=\"/\" className=\"hover:text-red-500\" aria-label=\"instagram\">\n          <Instagram />\n        </Link>\n        <Link href=\"/\" className=\"hover:text-red-500\" aria-label=\"twitter\">\n          <Twitter />\n        </Link>\n        <Link href=\"/\" className=\"hover:text-red-500\" aria-label=\"youtube\">\n          <Youtube />\n        </Link>\n      </i>\n      <div className=\"grid grid-cols-2 justify-between gap-y-3 py-3 text-xs text-white/50 md:flex md:text-sm\">\n        <div className=\"flex flex-col gap-3\">\n          <Link href=\"/\">Audio Description</Link>\n          <Link href=\"/\">Investor Relations</Link>\n          <Link href=\"/\">Legal Notices</Link>\n        </div>\n        <div className=\"flex flex-col gap-3\">\n          <Link href=\"/\">Help Center</Link>\n          <Link href=\"/\">Jobs</Link>\n          <Link href=\"/\">Cookie Preferences</Link>\n        </div>\n        <div className=\"flex flex-col gap-3\">\n          <Link href=\"/\">Gift Cards</Link>\n          <Link href=\"/\">Terms of Use</Link>\n          <Link href=\"/\">Corporate Information</Link>\n        </div>\n        <div className=\"flex flex-col gap-3\">\n          <Link href=\"/\">Media Center</Link>\n          <Link href=\"/\">Privacy</Link>\n          <Link href=\"/\">Contact Us</Link>\n        </div>\n      </div>\n      <div className=\"text-center font-semibold text-neutral-300\">\n        Built by Apestein. The source code is available on&nbsp;\n        <a\n          href=\"https://github.com/Apestein/nextflix\"\n          target=\"_blank\"\n          rel=\"noreferrer\"\n          className=\"font-medium underline underline-offset-4\"\n        >\n          Github\n        </a>\n      </div>\n    </footer>\n  )\n}\n\nasync function createAccountAndProfile() {\n  const user = await currentUser()\n  if (!user) throw new Error(ERR.unauthenticated)\n  await db\n    .insert(accounts)\n    .values({\n      id: user.id,\n      email: user.emailAddresses[0]!.emailAddress,\n      activeProfileId: user.id + \"-1\",\n    })\n    .onConflictDoNothing()\n  await db\n    .insert(profiles)\n    .values({\n      id: user.id + \"-1\",\n      accountId: user.id,\n      profileImgPath: `https://api.dicebear.com/6.x/bottts-neutral/svg?seed=${\n        user.username ?? user.firstName ?? user.emailAddresses[0]!.emailAddress\n      }`,\n      name:\n        user.username ?? user.firstName ?? user.emailAddresses[0]!.emailAddress,\n    })\n    .onConflictDoNothing()\n  return getAccountWithActiveProfile()\n}\n"
  },
  {
    "path": "src/app/(main)/loading.tsx",
    "content": "import { Skeleton } from \"~/components/ui/skeleton\"\n\nexport default function Loading() {\n  return (\n    <main>\n      <div className=\"space-y-10\">\n        <Skeleton className=\"my-16 h-[250px] w-full max-w-lg md:h-[384px]\" />\n        <Skeleton className=\"h-[138px] w-full md:h-[189px]\" />\n        <Skeleton className=\"h-[138px] w-full md:h-[189px]\" />\n        <Skeleton className=\"h-[138px] w-full md:h-[189px]\" />\n      </div>\n    </main>\n  )\n}\n"
  },
  {
    "path": "src/app/(main)/movies/page.tsx",
    "content": "import { ShowsCarousel } from \"~/components/show-carousel\"\nimport { getShows } from \"~/lib/client-fetchers\"\nimport { ShowBg } from \"../../../components/show-bg\"\nimport { ShowHero } from \"../../../components/show-hero\"\nimport { pickRandomShow } from \"~/lib/utils\"\n\nexport default async function Movies() {\n  const allShows = await getShows(\"movie\")\n  const randomShow = pickRandomShow(allShows.trending)\n\n  return (\n    <>\n      <ShowBg show={randomShow} />\n      <main>\n        <ShowHero show={randomShow} />\n        <div className=\"space-y-10\">\n          <ShowsCarousel title=\"Trending\" shows={allShows.trending} />\n          <ShowsCarousel title=\"Top Rated\" shows={allShows.topRated} />\n          <ShowsCarousel\n            title=\"Action Thriller\"\n            shows={allShows.actionThriller}\n          />\n          <ShowsCarousel title=\"Comedy\" shows={allShows.comedy} />\n          <ShowsCarousel title=\"Horror\" shows={allShows.horror} />\n          <ShowsCarousel title=\"Romance\" shows={allShows.romance} />\n          <ShowsCarousel title=\"Documentary\" shows={allShows.documentary} />\n        </div>\n      </main>\n    </>\n  )\n}\n"
  },
  {
    "path": "src/app/(main)/my-list/infinite-scroller.tsx",
    "content": "\"use client\"\nimport { useEffect, useRef, useState } from \"react\"\nimport type { Show } from \"~/lib/types\"\nimport { getMyShowsInfinite } from \"~/actions\"\nimport { getShows } from \"~/lib/client-fetchers\"\nimport { ERR } from \"~/lib/utils\"\nimport { Button } from \"~/components/ui/button\"\nimport Link from \"next/link\"\n\nexport function ShowScroller({\n  initialShows,\n  initialHasNextPage,\n  limit,\n}: {\n  initialShows: Show[]\n  initialHasNextPage: boolean\n  limit: number\n}) {\n  const [myShows, setMyShows] = useState(initialShows)\n  const [simulatedShows, setSimulatedShows] = useState<Show[]>()\n  const getShowsReturnRef = useRef<Awaited<ReturnType<typeof getShows>>>()\n  const hasNextPageRef = useRef(initialHasNextPage)\n  const indexRef = useRef(0)\n  const observerTarget = useRef(null)\n\n  const shows = simulatedShows ?? myShows\n\n  async function fetchNextPage() {\n    indexRef.current += 1\n    const { data } = await getMyShowsInfinite({\n      index: indexRef.current,\n      limit,\n    })\n    if (!data) throw new Error(ERR.db)\n    setMyShows((prev) => [...prev, ...data.shows])\n    hasNextPageRef.current = data.hasNextPage\n  }\n\n  async function getSimulatedShows() {\n    if (getShowsReturnRef.current) {\n      window.scrollTo(0, 0)\n      getShowsReturnRef.current = undefined\n      hasNextPageRef.current = initialHasNextPage\n      setSimulatedShows(undefined)\n      return\n    }\n    window.scrollTo(0, 0)\n    const data = await getShows(\"movie\")\n    getShowsReturnRef.current = data\n    hasNextPageRef.current = true\n    setSimulatedShows([\n      ...new Map(\n        [...data.trending, ...data.topRated].map((item) => [item.id, item]),\n      ).values(),\n    ])\n  }\n\n  useEffect(() => {\n    const observer = new IntersectionObserver(\n      (entries) => {\n        if (!hasNextPageRef.current) return\n        if (entries[0]?.isIntersecting) {\n          if (getShowsReturnRef.current) {\n            setTimeout(\n              () =>\n                setSimulatedShows((prev) => [\n                  ...new Map(\n                    [\n                      ...prev!,\n                      ...getShowsReturnRef.current!.actionThriller,\n                      ...getShowsReturnRef.current!.comedy,\n                    ].map((item) => [item.id, item]),\n                  ).values(),\n                ]),\n              1000,\n            )\n            hasNextPageRef.current = false\n          } else void fetchNextPage()\n        }\n      },\n      { threshold: 1 },\n    )\n\n    if (observerTarget.current) {\n      observer.observe(observerTarget.current)\n    }\n\n    return () => observer.disconnect()\n  }, [])\n\n  return (\n    <main className=\"space-y-1.5 [overflow-anchor:none]\">\n      <ul className=\"grid grid-cols-[repeat(auto-fill,_minmax(160px,_1fr))] gap-5 md:grid-cols-[repeat(auto-fill,_minmax(240px,_1fr))]\">\n        {shows.map((show) => (\n          <Link\n            href={`/show/${show.id}?mediaType=${show.title ? \"movie\" : \"tv\"}`}\n            scroll={false}\n            key={show.id}\n          >\n            {/* eslint-disable-next-line @next/next/no-img-element */}\n            <img\n              src={`https://image.tmdb.org/t/p/w300${\n                show.backdrop_path ?? show.poster_path\n              }`}\n              alt=\"show-backdrop\"\n              width={300}\n              height={169}\n              className=\"aspect-video max-w-full cursor-pointer object-cover transition-transform hover:scale-110\"\n            />\n          </Link>\n        ))}\n      </ul>\n      <div ref={observerTarget}></div>\n      <section className=\"flex flex-col items-center gap-4\">\n        {hasNextPageRef.current ? (\n          <Button\n            variant=\"outline\"\n            className=\"w-full animate-pulse\"\n            // eslint-disable-next-line @typescript-eslint/no-misused-promises\n            onClick={fetchNextPage}\n          >\n            Loading...\n          </Button>\n        ) : (\n          <Button variant=\"outline\" className=\"w-full cursor-auto\">\n            You have reached the end of your saved shows\n          </Button>\n        )}\n        {/* eslint-disable-next-line @typescript-eslint/no-misused-promises */}\n        <Button onClick={getSimulatedShows}>\n          {simulatedShows ? \"Turn off simulation\" : \"Simulate many saved shows\"}\n        </Button>\n      </section>\n    </main>\n  )\n}\n"
  },
  {
    "path": "src/app/(main)/my-list/loading.tsx",
    "content": "import { Skeleton } from \"~/components/ui/skeleton\"\n\nexport default function Loading() {\n  return (\n    <main className=\"pt-8\">\n      <Skeleton className=\"h-96 w-full\" />\n    </main>\n  )\n}\n"
  },
  {
    "path": "src/app/(main)/my-list/page.tsx",
    "content": "import { ShowScroller } from \"./infinite-scroller\"\nimport { getMyShows } from \"~/lib/server-fetchers\"\n\nexport default async function MyShowPage() {\n  const LIMIT = 30\n  const data = await getMyShows(LIMIT)\n  return (\n    <main className=\"pt-8\">\n      {!data.shows.length && (\n        <div className=\"mb-4 space-y-3\">\n          <p className=\"text-3xl font-semibold\">Your list is empty</p>\n          <p className=\"text-white/60\">\n            Add shows and movies to your list to watch them later\n          </p>\n        </div>\n      )}\n      <ShowScroller\n        initialShows={data.shows}\n        initialHasNextPage={data.hasNextPage}\n        limit={LIMIT}\n      />\n    </main>\n  )\n}\n"
  },
  {
    "path": "src/app/(main)/new-and-popular/page.tsx",
    "content": "import type { Show } from \"~/lib/types\"\nimport { ShowsCarousel } from \"~/components/show-carousel\"\nimport { ERR } from \"~/lib/utils\"\nimport { env } from \"~/env.mjs\"\nimport { ShowBg } from \"../../../components/show-bg\"\nimport { ShowHero } from \"../../../components/show-hero\"\nimport { pickRandomShow } from \"~/lib/utils\"\n\nexport default async function NewAndPopular() {\n  const newAndPopularShows = await getNewAndPopularShows()\n  const randomShow = pickRandomShow(newAndPopularShows.trendingMovies)\n\n  return (\n    <>\n      <ShowBg show={randomShow} />\n      <main>\n        <ShowHero show={randomShow} />\n        <div className=\"space-y-10\">\n          <ShowsCarousel\n            title=\"Popular Movies\"\n            shows={newAndPopularShows.popularMovies}\n          />\n          <ShowsCarousel\n            title=\"Popular TV Shows\"\n            shows={newAndPopularShows.popularTvs}\n          />\n          <ShowsCarousel\n            title=\"Trending Movies\"\n            shows={newAndPopularShows.trendingMovies}\n          />\n          <ShowsCarousel\n            title=\"Trending TV Shows\"\n            shows={newAndPopularShows.trendingTvs}\n          />\n        </div>\n      </main>\n    </>\n  )\n}\n\nasync function getNewAndPopularShows() {\n  const [popularTvRes, popularMovieRes, trendingTvRes, trendingMovieRes] =\n    await Promise.all([\n      fetch(\n        `https://api.themoviedb.org/3/tv/popular?api_key=${env.NEXT_PUBLIC_TMDB_API}`,\n      ),\n      fetch(\n        `https://api.themoviedb.org/3/movie/popular?api_key=${env.NEXT_PUBLIC_TMDB_API}`,\n      ),\n      fetch(\n        `https://api.themoviedb.org/3/trending/tv/day?api_key=${env.NEXT_PUBLIC_TMDB_API}`,\n      ),\n      fetch(\n        `https://api.themoviedb.org/3/trending/movie/day?api_key=${env.NEXT_PUBLIC_TMDB_API}`,\n      ),\n    ])\n\n  if (\n    !popularTvRes.ok ||\n    !popularMovieRes.ok ||\n    !trendingTvRes.ok ||\n    !trendingMovieRes.ok\n  ) {\n    throw new Error(ERR.fetch)\n  }\n\n  const [popularTvs, popularMovies, trendingTvs, trendingMovies] =\n    await Promise.all<{ results: Show[] }>([\n      popularTvRes.json(),\n      popularMovieRes.json(),\n      trendingTvRes.json(),\n      trendingMovieRes.json(),\n    ])\n\n  if (!popularTvs || !popularMovies || !trendingTvs || !trendingMovies)\n    throw new Error(ERR.fetch)\n\n  return {\n    popularTvs: popularTvs.results,\n    popularMovies: popularMovies.results,\n    trendingTvs: trendingTvs.results,\n    trendingMovies: trendingMovies.results,\n  }\n}\n"
  },
  {
    "path": "src/app/(main)/page.tsx",
    "content": "import { getShows } from \"~/lib/client-fetchers\"\nimport { ShowHero } from \"~/components/show-hero\"\nimport { ShowBg } from \"~/components/show-bg\"\nimport { pickRandomShow } from \"~/lib/utils\"\nimport { ShowsCarousel } from \"~/components/show-carousel\"\n\nexport default async function Home() {\n  const allShows = await getShows(\"movie\")\n  const randomShow = pickRandomShow(allShows.trending)\n\n  return (\n    <>\n      <ShowBg show={randomShow} />\n      <main>\n        <ShowHero show={randomShow} />\n        <div className=\"space-y-10\">\n          <ShowsCarousel title=\"Trending\" shows={allShows.trending} />\n          <ShowsCarousel title=\"Top Rated\" shows={allShows.topRated} />\n          <ShowsCarousel\n            title=\"Action Thriller\"\n            shows={allShows.actionThriller}\n          />\n          <ShowsCarousel title=\"Comedy\" shows={allShows.comedy} />\n          <ShowsCarousel title=\"Horror\" shows={allShows.horror} />\n          <ShowsCarousel title=\"Romance\" shows={allShows.romance} />\n          <ShowsCarousel title=\"Documentary\" shows={allShows.documentary} />\n        </div>\n      </main>\n    </>\n  )\n}\n"
  },
  {
    "path": "src/app/(main)/search/loading.tsx",
    "content": "import { Skeleton } from \"~/components/ui/skeleton\"\n\nexport default function Loading() {\n  return (\n    <main className=\"mt-8\">\n      <Skeleton className=\"h-96 w-full\" />\n    </main>\n  )\n}\n"
  },
  {
    "path": "src/app/(main)/search/page.tsx",
    "content": "import { ERR } from \"~/lib/utils\"\nimport { env } from \"~/env.mjs\"\nimport type { Show } from \"~/lib/types\"\nimport { SearchInput } from \"./search-input\"\nimport Link from \"next/link\"\n\nexport default async function SearchPage({\n  searchParams,\n}: {\n  searchParams: { keyword: string }\n}) {\n  if (!searchParams.keyword)\n    return (\n      <main>\n        <SearchInput initialQuery=\"\" className=\"my-8\" />\n      </main>\n    )\n  const shows = await searchShows(searchParams.keyword)\n  return (\n    <main>\n      <SearchInput initialQuery={searchParams.keyword} className=\"my-8\" />\n      <div className=\"grid grid-cols-[repeat(auto-fill,_minmax(160px,_1fr))] gap-4 md:grid-cols-[repeat(auto-fill,_minmax(240px,_1fr))]\">\n        {shows.map((show) =>\n          show.backdrop_path || show.poster_path ? (\n            <Link\n              href={`/show/${show.id}?mediaType=${show.title ? \"movie\" : \"tv\"}`}\n              scroll={false}\n              key={show.id}\n            >\n              {/* eslint-disable-next-line @next/next/no-img-element */}\n              <img\n                src={`https://image.tmdb.org/t/p/w300${\n                  show.backdrop_path ?? show.poster_path\n                }`}\n                alt=\"show-backdrop\"\n                width={300}\n                height={169}\n                className=\"aspect-video max-w-full cursor-pointer object-cover transition-transform hover:scale-110\"\n              />\n            </Link>\n          ) : null,\n        )}\n      </div>\n    </main>\n  )\n}\n\nasync function searchShows(query: string) {\n  const res = await fetch(\n    `https://api.themoviedb.org/3/search/multi?api_key=${env.NEXT_PUBLIC_TMDB_API}&query=${query}`,\n  )\n  if (!res.ok) throw new Error(ERR.fetch)\n  const shows = (await res.json()) as { results: Show[] }\n  const popularShows = shows.results.sort((a, b) => b.popularity - a.popularity)\n  return popularShows\n}\n"
  },
  {
    "path": "src/app/(main)/search/search-input.tsx",
    "content": "\"use client\"\nimport { Input } from \"~/components/ui/input\"\nimport { useEffect, useState } from \"react\"\nimport { useDebouncedCallback } from \"use-debounce\"\nimport { useRouter } from \"next/navigation\"\n\ninterface PageProps extends React.HTMLAttributes<HTMLElement> {\n  initialQuery: string\n}\nexport function SearchInput({ initialQuery, ...props }: PageProps) {\n  const [query, setQuery] = useState(\"\")\n  const debounced = useDebouncedCallback((value: string) => {\n    setQuery(value)\n  }, 500)\n  const router = useRouter()\n\n  useEffect(() => {\n    if (query) router.replace(`/search?keyword=${query}`)\n  }, [query])\n  return (\n    <Input\n      placeholder=\"search keyword\"\n      defaultValue={initialQuery}\n      onChange={(e) => debounced(e.target.value)}\n      autoFocus\n      {...props}\n    />\n  )\n}\n"
  },
  {
    "path": "src/app/(main)/show/[id]/page.tsx",
    "content": "import { ModalCard } from \"~/components/modal-card\"\nimport type { MediaType } from \"~/lib/types\"\nimport { getShowVideoAndGenreWithStatus } from \"~/lib/server-fetchers\"\n\nexport default async function ShowPage(props: {\n  params: { id: number }\n  searchParams: { mediaType: MediaType }\n}) {\n  const { show, isSaved } = await getShowVideoAndGenreWithStatus(\n    props.params.id,\n    props.searchParams.mediaType,\n  )\n  return (\n    <main className=\"mt-4\">\n      <ModalCard\n        show={show}\n        isSaved={isSaved}\n        className=\"mx-auto w-full max-w-3xl\"\n      />\n    </main>\n  )\n}\n"
  },
  {
    "path": "src/app/(main)/subscription/loading.tsx",
    "content": "import { Skeleton } from \"~/components/ui/skeleton\"\n\nexport default function Loading() {\n  return (\n    <main>\n      <Skeleton className=\"h-[50vh] w-full\" />\n    </main>\n  )\n}\n"
  },
  {
    "path": "src/app/(main)/subscription/page.tsx",
    "content": "import { Check } from \"lucide-react\"\nimport { PlanSelector } from \"./plan-selector\"\nimport { getAccount } from \"~/lib/server-fetchers\"\n\nexport default async function SubscriptionPage() {\n  const account = await getAccount()\n  return (\n    <main className=\"mt-8 space-y-8 md:px-24\">\n      <h1 className=\"text-3xl font-bold sm:text-4xl\">\n        Choose the plan that&apos;s right for you\n      </h1>\n      <div className=\"space-y-3 text-zinc-400\">\n        <div className=\"flex gap-1.5\">\n          <Check stroke=\"red\" />\n          <p>Watch on your phone, tablet, laptop, and TV</p>\n        </div>\n        <div className=\"flex gap-1.5\">\n          <Check stroke=\"red\" />\n          <p>Unlimited movies and TV shows</p>\n        </div>\n        <div className=\"flex gap-1.5\">\n          <Check stroke=\"red\" />\n          <p>Change or cancel your plan anytime</p>\n        </div>\n      </div>\n      <PlanSelector activeSubscription={account.membership} />\n      <div className=\"space-y-3 text-sm text-zinc-300\">\n        <p>\n          HD (720p), Full HD (1080p), Ultra HD (4K) and HDR availability subject\n          to your internet service and device capabilities. Not all content is\n          available in all resolutions. See our{\" \"}\n          <span className=\"cursor-pointer text-blue-500\">Terms of Use</span> for\n          more details.\n        </p>\n        <p>\n          Only people who live with you may use your account. Watch on 4\n          different devices at the same time with Premium, 2 with Standard, and\n          1 with Basic and Mobile.\n        </p>\n      </div>\n    </main>\n  )\n}\n"
  },
  {
    "path": "src/app/(main)/subscription/plan-selector.tsx",
    "content": "\"use client\"\nimport { cn } from \"~/lib/utils\"\nimport { useState } from \"react\"\nimport { Button } from \"~/components/ui/button\"\nimport type { SubscriptionPlan, PlanName } from \"~/lib/types\"\nimport { PLANS } from \"~/lib/configs\"\nimport { createCheckoutSession } from \"~/actions\"\n\nexport function PlanSelector({\n  activeSubscription,\n}: {\n  activeSubscription: PlanName\n}) {\n  const [selectedPlan, setSelectedPlan] = useState<SubscriptionPlan>(\n    Plans[activeSubscription],\n  )\n\n  function submit() {\n    void createCheckoutSession({\n      stripeProductId: selectedPlan.id,\n      planName: selectedPlan.name,\n    })\n  }\n\n  return (\n    <>\n      <div className=\"flex justify-end gap-1.5 md:gap-8\">\n        {PLANS.map((plan) => (\n          <div\n            key={plan.id}\n            className={cn(\n              \"grid aspect-square w-20 shrink-0 cursor-pointer place-content-center rounded-lg font-semibold md:w-24\",\n              selectedPlan.name === plan.name\n                ? \"bg-red-600\"\n                : \"bg-red-900 hover:bg-red-700\",\n            )}\n            onClick={() => setSelectedPlan(plan)}\n          >\n            {`${plan.name.charAt(0).toUpperCase()}${plan.name.substring(1)}`}\n          </div>\n        ))}\n      </div>\n      <div className=\"flex justify-end\">\n        <Button\n          className=\"w-56 bg-green-600 font-semibold text-white hover:bg-green-700\"\n          onClick={submit}\n          disabled={\n            selectedPlan.name === \"free\" && activeSubscription === \"free\"\n              ? true\n              : false\n          }\n        >\n          {activeSubscription !== \"free\" ? \"Edit\" : \"Subscribe\"}\n        </Button>\n      </div>\n    </>\n  )\n}\n\nconst Plans = {\n  free: PLANS[0],\n  basic: PLANS[1],\n  standard: PLANS[2],\n  premium: PLANS[3],\n}\n"
  },
  {
    "path": "src/app/(main)/subscription/result/page.tsx",
    "content": "// import type { Stripe } from \"stripe\"\nimport { stripe } from \"~/lib/stripe\"\nimport { ScrollArea } from \"~/components/ui/scroll-area\"\n\nexport default async function ResultPage({\n  searchParams,\n}: {\n  searchParams: { session_id: string }\n}): Promise<JSX.Element> {\n  if (!searchParams.session_id)\n    throw new Error(\"Please provide a valid session_id (`cs_test_...`)\")\n\n  const checkoutSession = await stripe.checkout.sessions.retrieve(\n    searchParams.session_id,\n    {\n      expand: [\"line_items\", \"payment_intent\"],\n    },\n  )\n\n  const checkoutStatus = checkoutSession.payment_status\n  const currencyFormatter = new Intl.NumberFormat(\"en-US\", {\n    style: \"currency\",\n    currency: \"USD\",\n  })\n  const amountPaid = currencyFormatter.format(\n    checkoutSession.amount_subtotal! / 100,\n  )\n  const createdAt = new Date(checkoutSession.created)\n  return (\n    <main className=\"mt-8 space-y-4\">\n      <h2 className=\"text-xl font-semibold\">\n        Checkout Status: {checkoutStatus}\n      </h2>\n      <h3>Checkout Session Info:</h3>\n      <ScrollArea className=\"rounded-md border p-4\">\n        {/* <PrintObject content={checkoutSession} /> */}\n        <p>{`Email: ${checkoutSession.customer_email}`}</p>\n        <p>{`Amount Total: ${amountPaid}`}</p>\n        <p>{`Time: ${createdAt.toUTCString()}`}</p>\n      </ScrollArea>\n    </main>\n  )\n}\n\n// function PrintObject({\n//   content,\n// }: {\n//   content: Stripe.Response<Stripe.Checkout.Session>\n// }): JSX.Element {\n//   const formattedContent: string = JSON.stringify(content, null, 2)\n//   return <pre>{formattedContent}</pre>\n// }\n"
  },
  {
    "path": "src/app/(main)/tv-shows/page.tsx",
    "content": "import { ShowsCarousel } from \"~/components/show-carousel\"\nimport { getShows } from \"~/lib/client-fetchers\"\nimport { ShowBg } from \"../../../components/show-bg\"\nimport { ShowHero } from \"../../../components/show-hero\"\nimport { pickRandomShow } from \"~/lib/utils\"\n\nexport default async function TvShows() {\n  const allShows = await getShows(\"tv\")\n  const randomShow = pickRandomShow(allShows.topRated)\n\n  return (\n    <>\n      <ShowBg show={randomShow} />\n      <main>\n        <ShowHero show={randomShow} />\n        <div className=\"space-y-10\">\n          <ShowsCarousel title=\"Trending\" shows={allShows.trending} />\n          <ShowsCarousel title=\"Top Rated\" shows={allShows.topRated} />\n          <ShowsCarousel\n            title=\"Action Thriller\"\n            shows={allShows.actionThriller}\n          />\n          <ShowsCarousel title=\"Comedy\" shows={allShows.comedy} />\n          <ShowsCarousel title=\"Horror\" shows={allShows.horror} />\n          <ShowsCarousel title=\"Romance\" shows={allShows.romance} />\n          <ShowsCarousel title=\"Documentary\" shows={allShows.documentary} />\n        </div>\n      </main>\n    </>\n  )\n}\n"
  },
  {
    "path": "src/app/(profile)/loading.tsx",
    "content": "import { Skeleton } from \"~/components/ui/skeleton\"\n\nexport default function Loading() {\n  return (\n    <main className=\"grid min-h-screen place-content-center\">\n      <Skeleton className=\"h-[50vh] w-[98vw] md:w-[500px]\" />\n    </main>\n  )\n}\n"
  },
  {
    "path": "src/app/(profile)/manage-profile/[...slug]/page.tsx",
    "content": "\"use client\"\nimport { Button } from \"~/components/ui/button\"\nimport { useState } from \"react\"\nimport { useDebouncedCallback } from \"use-debounce\"\nimport { useRouter } from \"next/navigation\"\nimport { useToast } from \"~/components/ui/use-toast\"\nimport { Input } from \"~/components/ui/input\"\nimport { ArrowLeft } from \"lucide-react\"\nimport Link from \"next/link\"\nimport { deleteProfile, updateProfile } from \"~/actions\"\n\nexport default function ProfilePage({\n  params,\n  searchParams,\n}: {\n  params: { slug: string[] }\n  searchParams: { profileId: string }\n}) {\n  const [name, setName] = useState(params.slug[0]!)\n  const debounced = useDebouncedCallback((value: string) => {\n    setName(value)\n  }, 500)\n  const router = useRouter()\n  const { toast } = useToast()\n\n  async function doDelete() {\n    const { data, validationError } = await deleteProfile({\n      profileId: searchParams.profileId,\n    })\n    toast({\n      description: data?.message ?? validationError?.profileId,\n    })\n    if (data) router.replace(\"/manage-profile\")\n  }\n\n  async function doUpdate() {\n    const { data, validationError } = await updateProfile({\n      profileId: searchParams.profileId,\n      name,\n    })\n    toast({\n      description: data?.message ?? JSON.stringify(validationError, null, 4),\n    })\n    if (data) router.replace(\"/manage-profile\")\n  }\n\n  return (\n    <>\n      <Button variant=\"outline\" asChild className=\"absolute ml-6 mt-6\">\n        <Link href=\"/manage-profile\">\n          <ArrowLeft />\n        </Link>\n      </Button>\n      <main className=\"grid min-h-screen place-content-center place-items-center gap-y-8\">\n        <div className=\"w-full space-y-3 border-b border-white/25 pb-3 text-center\">\n          <h1 className=\"text-3xl md:text-5xl\">Update Profile</h1>\n          <p className=\"text-white/60\">\n            Update a profile with a new name and avatar.\n          </p>\n        </div>\n        {/* eslint-disable-next-line @next/next/no-img-element */}\n        <img\n          src={`https://api.dicebear.com/6.x/bottts-neutral/svg?seed=${name}`}\n          alt=\"profile-image\"\n          width=\"135\"\n          height=\"135\"\n        />\n        <Input\n          defaultValue={name}\n          onChange={(e) => debounced(e.target.value)}\n        />\n        <section className=\"space-x-8\">\n          <Button\n            className=\"bg-green-600 font-semibold text-white hover:bg-green-700 active:bg-green-800\"\n            // eslint-disable-next-line @typescript-eslint/no-misused-promises\n            onClick={doUpdate}\n          >\n            Update\n          </Button>\n          <Button\n            className=\"bg-red-600 font-semibold text-white hover:bg-red-700 active:bg-red-800\"\n            // eslint-disable-next-line @typescript-eslint/no-misused-promises\n            onClick={doDelete}\n          >\n            Delete\n          </Button>\n        </section>\n      </main>\n    </>\n  )\n}\n"
  },
  {
    "path": "src/app/(profile)/manage-profile/add/page.tsx",
    "content": "\"use client\"\nimport { Button } from \"~/components/ui/button\"\nimport { Input } from \"~/components/ui/input\"\nimport { useState } from \"react\"\nimport { useDebouncedCallback } from \"use-debounce\"\nimport { useRouter } from \"next/navigation\"\nimport Link from \"next/link\"\nimport { ArrowLeft } from \"lucide-react\"\nimport { useToast } from \"~/components/ui/use-toast\"\nimport { createProfile } from \"~/actions\"\n\nexport default function AddProfilePage() {\n  const [name, setName] = useState(\"\")\n  const debounced = useDebouncedCallback((value: string) => {\n    setName(value)\n  }, 500)\n  const router = useRouter()\n  const { toast } = useToast()\n\n  async function doAdd() {\n    const { data, validationError } = await createProfile({ name })\n    toast({\n      description:\n        data?.message ?? validationError?.name ?? \"Name must be unique\",\n    })\n    if (data) router.replace(\"/manage-profile\")\n  }\n\n  return (\n    <>\n      <Button variant=\"outline\" asChild className=\"absolute ml-6 mt-6\">\n        <Link href=\"/manage-profile\">\n          <ArrowLeft />\n        </Link>\n      </Button>\n      <main className=\"grid min-h-screen place-content-center place-items-center gap-y-8\">\n        <div className=\"w-full space-y-3 border-b border-white/25 pb-3 text-center\">\n          <h1 className=\"text-3xl md:text-5xl\">Add Profile</h1>\n          <p className=\"text-white/60\">\n            Add a profile for another person watching Netflix.\n          </p>\n        </div>\n        {/* eslint-disable-next-line @next/next/no-img-element */}\n        <img\n          src={`https://api.dicebear.com/6.x/bottts-neutral/svg?seed=${name}`}\n          alt=\"profile-image\"\n          width=\"135\"\n          height=\"135\"\n        />\n        <Input placeholder=\"name\" onChange={(e) => debounced(e.target.value)} />\n        {/* eslint-disable-next-line @typescript-eslint/no-misused-promises */}\n        <Button onClick={doAdd}>Save</Button>\n      </main>\n    </>\n  )\n}\n"
  },
  {
    "path": "src/app/(profile)/manage-profile/page.tsx",
    "content": "import { Button } from \"~/components/ui/button\"\nimport { PlusCircle, ArrowLeft, Pencil } from \"lucide-react\"\nimport Link from \"next/link\"\nimport { getAccountWithProfiles } from \"~/lib/server-fetchers\"\n\nexport default async function ManageProfilePage() {\n  const account = await getAccountWithProfiles()\n  return (\n    <>\n      <Button variant=\"outline\" asChild className=\"absolute ml-6 mt-6\">\n        <Link href=\"/\">\n          <ArrowLeft />\n        </Link>\n      </Button>\n      <main className=\"grid min-h-screen place-content-center\">\n        <section className=\"space-y-8\">\n          <h1 className=\"text-center text-3xl md:text-5xl\">Manage Profiles</h1>\n          <ul className=\"grid grid-cols-2 gap-4 md:flex\">\n            {account.profiles.map((profile) => (\n              <div key={profile.id} className=\"space-y-1.5\">\n                <Link\n                  href={`/manage-profile/${profile.name}?profileId=${profile.id}`}\n                  className=\"relative\"\n                >\n                  {/* eslint-disable-next-line @next/next/no-img-element */}\n                  <img\n                    src={profile.profileImgPath}\n                    alt=\"profile-image\"\n                    width=\"96\"\n                    height=\"96\"\n                  />\n                  <div className=\"absolute inset-0 grid h-full w-24 place-items-center bg-black/25 hover:bg-transparent\">\n                    <Pencil />\n                  </div>\n                </Link>\n                <h3 className=\"text-center\">{profile.name}</h3>\n              </div>\n            ))}\n            {account.profiles.length !== 4 && (\n              <Link href=\"/manage-profile/add\">\n                <PlusCircle\n                  className=\"h-24 w-24 bg-neutral-800 p-3 outline-1 hover:outline\"\n                  strokeWidth={1}\n                />\n              </Link>\n            )}\n          </ul>\n        </section>\n      </main>\n    </>\n  )\n}\n"
  },
  {
    "path": "src/app/(profile)/switch-profile/page.tsx",
    "content": "import { Button } from \"~/components/ui/button\"\nimport { ArrowLeft } from \"lucide-react\"\nimport Link from \"next/link\"\nimport { ProfileSwitcher } from \"./profile-switcher\"\nimport { getAccountWithProfiles } from \"~/lib/server-fetchers\"\n\nexport default async function SwitchProfilePage() {\n  const account = await getAccountWithProfiles()\n  return (\n    <>\n      <Button variant=\"outline\" asChild className=\"absolute ml-6 mt-6\">\n        <Link href=\"/\">\n          <ArrowLeft />\n        </Link>\n      </Button>\n      <main className=\"grid min-h-screen place-content-center\">\n        <section className=\"space-y-8\">\n          <h1 className=\"text-center text-3xl md:text-5xl\">\n            Who&apos;s Watching\n          </h1>\n          <ul className=\"grid grid-cols-2 gap-4 md:flex\">\n            {account.profiles.map((profile) => (\n              <ProfileSwitcher key={profile.id} profile={profile} />\n            ))}\n          </ul>\n        </section>\n      </main>\n    </>\n  )\n}\n"
  },
  {
    "path": "src/app/(profile)/switch-profile/profile-switcher.tsx",
    "content": "\"use client\"\nimport type { Profile } from \"~/lib/types\"\nimport { useRouter } from \"next/navigation\"\nimport { useToast } from \"~/components/ui/use-toast\"\nimport { switchProfile } from \"~/actions\"\n\nexport function ProfileSwitcher({ profile }: { profile: Profile }) {\n  const router = useRouter()\n  const { toast } = useToast()\n\n  async function doSwitch() {\n    const { data, validationError } = await switchProfile({\n      profileId: profile.id,\n    })\n    toast({ description: data?.message ?? validationError?.profileId })\n    if (data) router.replace(\"/\")\n  }\n\n  return (\n    <div className=\"space-y-1.5\">\n      {/* eslint-disable-next-line @next/next/no-img-element */}\n      <img\n        src={profile.profileImgPath}\n        width=\"96\"\n        height=\"96\"\n        alt=\"profile-image\"\n        // eslint-disable-next-line @typescript-eslint/no-misused-promises\n        onClick={doSwitch}\n        className=\"cursor-pointer outline-1 hover:outline\"\n      />\n      <h3 className=\"text-center\">{profile.name}</h3>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/app/api/(webhook)/stripe/route.ts",
    "content": "import type { Stripe } from \"stripe\"\nimport { stripe } from \"~/lib/stripe\"\nimport { NextResponse } from \"next/server\"\nimport { db } from \"~/db/client\"\nimport { accounts } from \"~/db/schema\"\nimport { eq } from \"drizzle-orm\"\nimport { headers } from \"next/headers\"\nimport { env } from \"~/env.mjs\"\nimport { planTuple } from \"~/lib/configs\"\nimport { z } from \"zod\"\n\nexport const runtime = \"nodejs\"\n\nexport async function POST(req: Request) {\n  const body = await req.text()\n  const signature = headers().get(\"Stripe-Signature\")!\n\n  let event: Stripe.Event\n\n  try {\n    event = stripe.webhooks.constructEvent(\n      body,\n      signature,\n      env.NODE_ENV === \"production\"\n        ? env.STRIPE_WEBHOOK_SECRET\n        : env.STRIPE_DEV_WEBHOOK_SECRET,\n    )\n  } catch (error) {\n    return new Response(\n      `Webhook Error: ${\n        error instanceof Error ? error.message : \"Unknown error\"\n      }`,\n      { status: 400 },\n    )\n  }\n\n  if (event.type === \"checkout.session.completed\") {\n    const session = event.data.object as Stripe.Checkout.Session\n    // Retrieve the subscription details from Stripe.\n    const subscription = await stripe.subscriptions.retrieve(\n      session.subscription as string,\n    )\n\n    // Update the user stripe into in our database.\n    const userId = session.metadata!.userId!\n    const plan = session.metadata!.planName!\n    const planSchema = z.enum(planTuple)\n    const validatedPlan = planSchema.parse(plan)\n\n    await db\n      .update(accounts)\n      .set({\n        stripeCustomerId: subscription.customer as string,\n        membership: validatedPlan,\n      })\n      .where(eq(accounts.id, userId))\n  }\n\n  if (event.type === \"customer.subscription.updated\") {\n    const data = event.data.object as Stripe.Subscription\n    if (data.canceled_at)\n      await db\n        .update(accounts)\n        .set({ membership: \"free\" })\n        .where(eq(accounts.stripeCustomerId, data.customer as string))\n  }\n\n  return NextResponse.json({ message: \"Received\" }, { status: 200 })\n}\n"
  },
  {
    "path": "src/app/error.tsx",
    "content": "\"use client\"\n\nimport { useEffect } from \"react\"\nimport { Button } from \"~/components/ui/button\"\n\nexport default function Error({\n  error,\n  reset,\n}: {\n  error: Error & { digest?: string }\n  reset: () => void\n}) {\n  useEffect(() => {\n    console.error(error)\n  }, [error])\n\n  return (\n    <main className=\"container grid min-h-screen place-content-center space-y-5 text-center\">\n      <h1 className=\"text-3xl font-semibold\">There was a problem</h1>\n      <p>{error.message}</p>\n      <section className=\"space-x-8\">\n        <Button onClick={() => reset()} className=\"font-semibold\">\n          Try again\n        </Button>\n        <Button asChild variant=\"secondary\" className=\"font-semibold\">\n          <a href=\"/\">Go back home</a>\n        </Button>\n      </section>\n    </main>\n  )\n}\n"
  },
  {
    "path": "src/app/layout.tsx",
    "content": "import \"~/lib/globals.css\"\nimport { Inter } from \"next/font/google\"\nimport { cn } from \"~/lib/utils\"\nimport { ThemeProvider } from \"~/components/theme-provider\"\nimport { ClerkProvider } from \"@clerk/nextjs\"\nimport { Toaster } from \"~/components/ui/toaster\"\nimport { Analytics } from \"@vercel/analytics/react\"\n\nexport const runtime = \"edge\"\nexport const preferredRegion = \"iad1\"\nconst inter = Inter({ subsets: [\"latin\"] })\n\nconst siteConfig = {\n  title: \"Netflix Clone\",\n  description:\n    \"Open source project using bleeding-edge stack. Drizzle ORM + Neon postgres + Clerk auth + Shadcn/ui + everything new in Next.js 13 (server components, server actions, streaming ui, parallel routes, intercepting routes).\",\n  url: \"/\",\n  siteName: \"Nextflix\",\n}\nexport const metadata = {\n  metadataBase: new URL(\"https://nextflix-blush.vercel.app\"),\n  title: siteConfig.title,\n  description: siteConfig.description,\n  openGraph: {\n    title: siteConfig.title,\n    description: siteConfig.description,\n    url: siteConfig.url,\n    siteName: siteConfig.siteName,\n    locale: \"en_US\",\n    type: \"website\",\n  },\n  twitter: {\n    card: \"summary_large_image\",\n    title: siteConfig.title,\n  },\n}\n\nexport default function RootLayout({\n  children,\n}: {\n  children: React.ReactNode\n}) {\n  return (\n    <ClerkProvider>\n      <html lang=\"en\" suppressHydrationWarning>\n        <body\n          className={cn(\n            \"bg-neutral-900 text-slate-50 antialiased scrollbar-none\",\n            inter.className,\n          )}\n        >\n          <ThemeProvider attribute=\"class\" defaultTheme=\"dark\">\n            {children}\n          </ThemeProvider>\n          <Toaster />\n          <Analytics />\n        </body>\n      </html>\n    </ClerkProvider>\n  )\n}\n"
  },
  {
    "path": "src/app/not-found.tsx",
    "content": "import Link from \"next/link\"\nimport { Button } from \"~/components/ui/button\"\n\nexport default function NotFound() {\n  return (\n    <main className=\"grid min-h-screen place-content-center space-y-5 text-center\">\n      <h2 className=\"text-3xl font-semibold\">Not Found</h2>\n      <p>Could not find requested resource</p>\n      <Button asChild variant=\"outline\" className=\"font-semibold\">\n        <Link href=\"/\">Return Home</Link>\n      </Button>\n    </main>\n  )\n}\n"
  },
  {
    "path": "src/components/link-button.tsx",
    "content": "\"use client\"\nimport { useRouter } from \"next/navigation\"\n\nexport function LinkButton({\n  children,\n  href,\n}: {\n  children: React.ReactNode\n  href: string\n}) {\n  const router = useRouter()\n  return (\n    <button\n      onClick={() => {\n        router.push(href)\n        router.refresh()\n      }}\n    >\n      {children}\n    </button>\n  )\n}\n"
  },
  {
    "path": "src/components/modal-card.tsx",
    "content": "\"use client\"\nimport type { ShowWithVideoAndGenre } from \"~/lib/types\"\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"~/components/ui/card\"\nimport { PlusCircle, CheckCircle } from \"lucide-react\"\nimport { toggleMyShow } from \"~/actions\"\nimport { useOptimisticAction } from \"next-safe-action/hook\"\n\ninterface ModalCardProps extends React.ComponentPropsWithoutRef<\"div\"> {\n  show: ShowWithVideoAndGenre\n  isSaved?: boolean\n}\nexport function ModalCard({ show, isSaved, ...props }: ModalCardProps) {\n  const trailer = show.videos.results.find((el) => el.type === \"Trailer\")\n  return (\n    <Card {...props}>\n      <CardHeader>\n        <CardTitle className=\"flex items-center gap-1.5\">\n          {show.title ?? show.name}\n          {isSaved !== undefined && (\n            <MyShowToggler show={show} isSaved={isSaved} />\n          )}\n        </CardTitle>\n        <div className=\"flex items-center gap-1.5\">\n          <p className=\"text-green-400\">\n            {Math.round((show.vote_average * 100) / 10)}% Match\n          </p>\n          <p>\n            {show.release_date?.substring(0, 4) ??\n              show.first_air_date?.substring(0, 4)}\n          </p>\n          <p className=\"border border-neutral-500 px-1 text-xs text-white/50\">\n            EN\n          </p>\n        </div>\n        <CardDescription>{show.overview}</CardDescription>\n        <p className=\"text-left text-sm\">\n          {show.genres.map((genre) => genre.name).join(\", \")}\n        </p>\n      </CardHeader>\n      <CardContent className=\"p-0\">\n        {trailer ? (\n          <iframe\n            src={`https://www.youtube.com/embed/${trailer.key}`}\n            className=\"aspect-video w-full rounded-lg\"\n          />\n        ) : (\n          <div className=\"grid aspect-video animate-pulse place-content-center text-xl font-semibold\">\n            No Trailer\n          </div>\n        )}\n      </CardContent>\n    </Card>\n  )\n}\n\nfunction MyShowToggler({\n  show,\n  isSaved,\n}: {\n  show: ShowWithVideoAndGenre\n  isSaved: boolean\n}) {\n  const {\n    execute: executeToggle,\n    optimisticData,\n    res,\n    isExecuting,\n  } = useOptimisticAction(toggleMyShow, { isSaved })\n\n  function toggle() {\n    void executeToggle(\n      {\n        id: show.id,\n        isSaved: res.data?.isSaved ?? isSaved,\n        movieOrTv: show.title ? \"movie\" : \"tv\",\n      },\n      { isSaved: !res.data?.isSaved ?? !isSaved },\n    )\n  }\n\n  return (\n    <button onClick={toggle} disabled={isExecuting}>\n      {optimisticData.isSaved ? (\n        <CheckCircle\n          className=\"h-6 w-6 cursor-pointer\"\n          strokeWidth=\"1.5\"\n          opacity={isExecuting ? 0.5 : 1}\n        />\n      ) : (\n        <PlusCircle\n          className=\"h-6 w-6 cursor-pointer\"\n          strokeWidth=\"1.5\"\n          opacity={isExecuting ? 0.5 : 1}\n        />\n      )}\n    </button>\n  )\n}\n"
  },
  {
    "path": "src/components/overlay-scrollbar.tsx",
    "content": "\"use client\"\nimport { useOverlayScrollbars } from \"overlayscrollbars-react\"\nimport \"overlayscrollbars/overlayscrollbars.css\"\nimport { useEffect } from \"react\"\n\nexport function OverlayScrollbar() {\n  const [initBodyOverlayScrollbars] = useOverlayScrollbars({\n    defer: true,\n    options: {\n      scrollbars: {\n        theme: \"os-theme-light\",\n        autoHide: \"scroll\",\n      },\n    },\n  })\n\n  useEffect(() => {\n    //only run on none touch screen devices\n    if (window.matchMedia(\"(pointer: fine)\").matches)\n      initBodyOverlayScrollbars(document.body)\n  }, [initBodyOverlayScrollbars])\n\n  return null\n}\n"
  },
  {
    "path": "src/components/show-bg.tsx",
    "content": "import { type Show } from \"~/lib/types\"\nimport Image from \"next/image\"\n\nexport function ShowBg({ show }: { show: Show }) {\n  return (\n    <div\n      aria-label=\"background\"\n      className=\"absolute inset-0 -z-10 h-screen w-full\"\n    >\n      <div className=\"h-full w-full bg-black/60 bg-gradient-to-b from-neutral-900/0 to-neutral-900\" />\n      <Image\n        src={`https://image.tmdb.org/t/p/original/${show.backdrop_path}`}\n        alt=\"background-image\"\n        className=\"-z-10 object-cover\"\n        fill\n        priority\n      />\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/show-carousel.tsx",
    "content": "\"use client\"\nimport { useSnapCarousel } from \"react-snap-carousel\"\nimport type { Show } from \"~/lib/types\"\nimport { useRef } from \"react\"\nimport { useDraggable } from \"react-use-draggable-scroll\"\nimport { Button } from \"./ui/button\"\nimport { ChevronLeft, ChevronRight } from \"lucide-react\"\nimport Link from \"next/link\"\nimport { type MutableRefObject, type RefCallback } from \"react\"\n\nexport function ShowsCarousel({\n  title,\n  shows,\n}: {\n  title: string\n  shows: Show[]\n}) {\n  const { scrollRef, next, prev } = useSnapCarousel()\n\n  const dragRef =\n    useRef<HTMLDivElement>() as React.MutableRefObject<HTMLInputElement>\n  const { events } = useDraggable(dragRef)\n\n  return (\n    <section>\n      <div className=\"w-full max-w-screen-2xl space-y-1 sm:space-y-2.5\">\n        <h2 className=\"text-lg font-semibold md:text-xl\">{title}</h2>\n        <div className=\"group relative flex items-center\">\n          <Button\n            aria-label=\"scroll left\"\n            variant=\"ghost\"\n            className=\"absolute left-0 z-10 h-[90px] rounded-none rounded-r bg-slate-950/50 px-2 py-0 opacity-0 group-hover:opacity-100 hover:bg-slate-950/75 md:h-[135px] mobile:hidden\"\n            onClick={() => prev()}\n          >\n            <ChevronLeft className=\"h-8 w-8 text-white\" aria-hidden=\"true\" />\n          </Button>\n          <Button\n            aria-label=\"scroll right\"\n            variant=\"ghost\"\n            className=\"absolute right-0 z-10 h-[90px] rounded-none rounded-l bg-slate-950/50 px-2 py-0 opacity-0 group-hover:opacity-100 hover:bg-slate-950/75 md:h-[135px] mobile:hidden\"\n            onClick={() => next()}\n          >\n            <ChevronRight className=\"h-8 w-8 text-white\" aria-hidden=\"true\" />\n          </Button>\n          <div\n            className=\"flex gap-1.5 overflow-auto py-2 scrollbar-none\"\n            ref={mergeRefs(dragRef, scrollRef)}\n            {...events}\n          >\n            {shows.map((show) => (\n              <Link\n                href={`/show/${show.id}?mediaType=${\n                  show.title ? \"movie\" : \"tv\"\n                }`}\n                scroll={false}\n                key={show.id}\n              >\n                {/* eslint-disable-next-line @next/next/no-img-element */}\n                <img\n                  src={`https://image.tmdb.org/t/p/w300${\n                    show.backdrop_path ?? show.poster_path\n                  }`}\n                  alt=\"show-backdrop\"\n                  width={240}\n                  height={135}\n                  className=\"aspect-video min-w-[160px] object-cover transition-transform hover:scale-110 md:min-w-[240px]\"\n                />\n              </Link>\n            ))}\n          </div>\n        </div>\n      </div>\n    </section>\n  )\n}\n\ntype MutableRefList<T> = Array<\n  RefCallback<T> | MutableRefObject<T> | undefined | null\n>\nfunction mergeRefs<T>(...refs: MutableRefList<T>): RefCallback<T> {\n  return (val: T) => {\n    setRef(val, ...refs)\n  }\n}\n\nfunction setRef<T>(val: T, ...refs: MutableRefList<T>): void {\n  refs.forEach((ref) => {\n    if (typeof ref === \"function\") {\n      ref(val)\n    } else if (ref != null) {\n      ref.current = val\n    }\n  })\n}\n"
  },
  {
    "path": "src/components/show-hero.tsx",
    "content": "import { type Show } from \"~/lib/types\"\nimport { Play, Info } from \"lucide-react\"\nimport Link from \"next/link\"\nimport { Button } from \"~/components/ui/button\"\n\nexport function ShowHero({ show }: { show: Show }) {\n  return (\n    <div className=\"flex min-h-[384px] max-w-lg flex-col justify-center space-y-3\">\n      <p className=\"text-3xl font-bold md:text-4xl\">{show.title}</p>\n      <div className=\"flex space-x-2 text-xs font-semibold md:text-sm\">\n        <p className=\"text-green-600\">\n          {Math.round((show.vote_average * 100) / 10)}% Match\n        </p>\n        <p>{show.release_date ?? show.first_air_date}</p>\n      </div>\n      <p className=\"line-clamp-4 text-sm text-gray-300 md:text-base\">\n        {show.overview}\n      </p>\n      <div className=\"flex items-center gap-3\">\n        <Link\n          href={`/show/${show.id}?mediaType=${show.title ? \"movie\" : \"tv\"}`}\n          scroll={false}\n        >\n          <Button className=\"flex gap-1.5\">\n            <Play fill=\"black\" />\n            Play\n          </Button>\n        </Link>\n        <Link\n          href={`/show/${show.id}?mediaType=${show.title ? \"movie\" : \"tv\"}`}\n          scroll={false}\n        >\n          <Button variant=\"outline\" className=\"flex gap-1.5\">\n            <Info />\n            More Info\n          </Button>\n        </Link>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/theme-provider.tsx",
    "content": "\"use client\"\nimport { ThemeProvider as NextThemeProvider } from \"next-themes\"\nimport { type ThemeProviderProps } from \"next-themes/dist/types\"\n\nexport function ThemeProvider({ children, ...props }: ThemeProviderProps) {\n  return <NextThemeProvider {...props}>{children}</NextThemeProvider>\n}\n"
  },
  {
    "path": "src/components/ui/button.tsx",
    "content": "import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"src/lib/utils\"\n\nconst buttonVariants = cva(\n  \"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n        destructive:\n          \"bg-destructive text-destructive-foreground hover:bg-destructive/90\",\n        outline:\n          \"border border-input bg-background hover:bg-accent hover:text-accent-foreground\",\n        secondary:\n          \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n        ghost: \"hover:bg-accent hover:text-accent-foreground\",\n        link: \"text-primary underline-offset-4 hover:underline\",\n      },\n      size: {\n        default: \"h-10 px-4 py-2\",\n        sm: \"h-9 rounded-md px-3\",\n        lg: \"h-11 rounded-md px-8\",\n        icon: \"h-10 w-10\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  }\n)\n\nexport interface ButtonProps\n  extends React.ButtonHTMLAttributes<HTMLButtonElement>,\n    VariantProps<typeof buttonVariants> {\n  asChild?: boolean\n}\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n  ({ className, variant, size, asChild = false, ...props }, ref) => {\n    const Comp = asChild ? Slot : \"button\"\n    return (\n      <Comp\n        className={cn(buttonVariants({ variant, size, className }))}\n        ref={ref}\n        {...props}\n      />\n    )\n  }\n)\nButton.displayName = \"Button\"\n\nexport { Button, buttonVariants }\n"
  },
  {
    "path": "src/components/ui/card.tsx",
    "content": "import * as React from \"react\"\n\nimport { cn } from \"src/lib/utils\"\n\nconst Card = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\n      \"rounded-lg border bg-card text-card-foreground shadow-sm\",\n      className\n    )}\n    {...props}\n  />\n))\nCard.displayName = \"Card\"\n\nconst CardHeader = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\"flex flex-col space-y-1.5 p-6\", className)}\n    {...props}\n  />\n))\nCardHeader.displayName = \"CardHeader\"\n\nconst CardTitle = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLHeadingElement>\n>(({ className, ...props }, ref) => (\n  <h3\n    ref={ref}\n    className={cn(\n      \"text-2xl font-semibold leading-none tracking-tight\",\n      className\n    )}\n    {...props}\n  />\n))\nCardTitle.displayName = \"CardTitle\"\n\nconst CardDescription = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, ...props }, ref) => (\n  <p\n    ref={ref}\n    className={cn(\"text-sm text-muted-foreground\", className)}\n    {...props}\n  />\n))\nCardDescription.displayName = \"CardDescription\"\n\nconst CardContent = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div ref={ref} className={cn(\"p-6 pt-0\", className)} {...props} />\n))\nCardContent.displayName = \"CardContent\"\n\nconst CardFooter = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\" flex items-center p-6 pt-0\", className)}\n    {...props}\n  />\n))\nCardFooter.displayName = \"CardFooter\"\n\nexport { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }\n"
  },
  {
    "path": "src/components/ui/dropdown-menu.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\"\nimport { Check, ChevronRight, Circle } from \"lucide-react\"\n\nimport { cn } from \"src/lib/utils\"\n\nconst DropdownMenu = DropdownMenuPrimitive.Root\n\nconst DropdownMenuTrigger = DropdownMenuPrimitive.Trigger\n\nconst DropdownMenuGroup = DropdownMenuPrimitive.Group\n\nconst DropdownMenuPortal = DropdownMenuPrimitive.Portal\n\nconst DropdownMenuSub = DropdownMenuPrimitive.Sub\n\nconst DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup\n\nconst DropdownMenuSubTrigger = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {\n    inset?: boolean\n  }\n>(({ className, inset, children, ...props }, ref) => (\n  <DropdownMenuPrimitive.SubTrigger\n    ref={ref}\n    className={cn(\n      \"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent\",\n      inset && \"pl-8\",\n      className\n    )}\n    {...props}\n  >\n    {children}\n    <ChevronRight className=\"ml-auto h-4 w-4\" />\n  </DropdownMenuPrimitive.SubTrigger>\n))\nDropdownMenuSubTrigger.displayName =\n  DropdownMenuPrimitive.SubTrigger.displayName\n\nconst DropdownMenuSubContent = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>\n>(({ className, ...props }, ref) => (\n  <DropdownMenuPrimitive.SubContent\n    ref={ref}\n    className={cn(\n      \"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n      className\n    )}\n    {...props}\n  />\n))\nDropdownMenuSubContent.displayName =\n  DropdownMenuPrimitive.SubContent.displayName\n\nconst DropdownMenuContent = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>\n>(({ className, sideOffset = 4, ...props }, ref) => (\n  <DropdownMenuPrimitive.Portal>\n    <DropdownMenuPrimitive.Content\n      ref={ref}\n      sideOffset={sideOffset}\n      className={cn(\n        \"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n        className\n      )}\n      {...props}\n    />\n  </DropdownMenuPrimitive.Portal>\n))\nDropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName\n\nconst DropdownMenuItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {\n    inset?: boolean\n  }\n>(({ className, inset, ...props }, ref) => (\n  <DropdownMenuPrimitive.Item\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      inset && \"pl-8\",\n      className\n    )}\n    {...props}\n  />\n))\nDropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName\n\nconst DropdownMenuCheckboxItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>\n>(({ className, children, checked, ...props }, ref) => (\n  <DropdownMenuPrimitive.CheckboxItem\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      className\n    )}\n    checked={checked}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <DropdownMenuPrimitive.ItemIndicator>\n        <Check className=\"h-4 w-4\" />\n      </DropdownMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </DropdownMenuPrimitive.CheckboxItem>\n))\nDropdownMenuCheckboxItem.displayName =\n  DropdownMenuPrimitive.CheckboxItem.displayName\n\nconst DropdownMenuRadioItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>\n>(({ className, children, ...props }, ref) => (\n  <DropdownMenuPrimitive.RadioItem\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      className\n    )}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <DropdownMenuPrimitive.ItemIndicator>\n        <Circle className=\"h-2 w-2 fill-current\" />\n      </DropdownMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </DropdownMenuPrimitive.RadioItem>\n))\nDropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName\n\nconst DropdownMenuLabel = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {\n    inset?: boolean\n  }\n>(({ className, inset, ...props }, ref) => (\n  <DropdownMenuPrimitive.Label\n    ref={ref}\n    className={cn(\n      \"px-2 py-1.5 text-sm font-semibold\",\n      inset && \"pl-8\",\n      className\n    )}\n    {...props}\n  />\n))\nDropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName\n\nconst DropdownMenuSeparator = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <DropdownMenuPrimitive.Separator\n    ref={ref}\n    className={cn(\"-mx-1 my-1 h-px bg-muted\", className)}\n    {...props}\n  />\n))\nDropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName\n\nconst DropdownMenuShortcut = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLSpanElement>) => {\n  return (\n    <span\n      className={cn(\"ml-auto text-xs tracking-widest opacity-60\", className)}\n      {...props}\n    />\n  )\n}\nDropdownMenuShortcut.displayName = \"DropdownMenuShortcut\"\n\nexport {\n  DropdownMenu,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuCheckboxItem,\n  DropdownMenuRadioItem,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuShortcut,\n  DropdownMenuGroup,\n  DropdownMenuPortal,\n  DropdownMenuSub,\n  DropdownMenuSubContent,\n  DropdownMenuSubTrigger,\n  DropdownMenuRadioGroup,\n}\n"
  },
  {
    "path": "src/components/ui/input.tsx",
    "content": "/* eslint-disable @typescript-eslint/no-empty-interface */\nimport * as React from \"react\"\n\nimport { cn } from \"src/lib/utils\"\n\nexport interface InputProps\n  extends React.InputHTMLAttributes<HTMLInputElement> {}\n\nconst Input = React.forwardRef<HTMLInputElement, InputProps>(\n  ({ className, type, ...props }, ref) => {\n    return (\n      <input\n        type={type}\n        className={cn(\n          \"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50\",\n          className\n        )}\n        ref={ref}\n        {...props}\n      />\n    )\n  }\n)\nInput.displayName = \"Input\"\n\nexport { Input }\n"
  },
  {
    "path": "src/components/ui/label.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as LabelPrimitive from \"@radix-ui/react-label\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"src/lib/utils\"\n\nconst labelVariants = cva(\n  \"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\"\n)\n\nconst Label = React.forwardRef<\n  React.ElementRef<typeof LabelPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &\n    VariantProps<typeof labelVariants>\n>(({ className, ...props }, ref) => (\n  <LabelPrimitive.Root\n    ref={ref}\n    className={cn(labelVariants(), className)}\n    {...props}\n  />\n))\nLabel.displayName = LabelPrimitive.Root.displayName\n\nexport { Label }\n"
  },
  {
    "path": "src/components/ui/scroll-area.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as ScrollAreaPrimitive from \"@radix-ui/react-scroll-area\"\n\nimport { cn } from \"~/lib/utils\"\n\nconst ScrollArea = React.forwardRef<\n  React.ElementRef<typeof ScrollAreaPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>\n>(({ className, children, ...props }, ref) => (\n  <ScrollAreaPrimitive.Root\n    ref={ref}\n    className={cn(\"relative overflow-hidden\", className)}\n    {...props}\n  >\n    <ScrollAreaPrimitive.Viewport className=\"h-full w-full rounded-[inherit]\">\n      {children}\n    </ScrollAreaPrimitive.Viewport>\n    <ScrollBar />\n    <ScrollAreaPrimitive.Corner />\n  </ScrollAreaPrimitive.Root>\n))\nScrollArea.displayName = ScrollAreaPrimitive.Root.displayName\n\nconst ScrollBar = React.forwardRef<\n  React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,\n  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>\n>(({ className, orientation = \"vertical\", ...props }, ref) => (\n  <ScrollAreaPrimitive.ScrollAreaScrollbar\n    ref={ref}\n    orientation={orientation}\n    className={cn(\n      \"flex touch-none select-none transition-colors\",\n      orientation === \"vertical\" &&\n        \"h-full w-2.5 border-l border-l-transparent p-[1px]\",\n      orientation === \"horizontal\" &&\n        \"h-2.5 border-t border-t-transparent p-[1px]\",\n      className\n    )}\n    {...props}\n  >\n    <ScrollAreaPrimitive.ScrollAreaThumb className=\"relative flex-1 rounded-full bg-border\" />\n  </ScrollAreaPrimitive.ScrollAreaScrollbar>\n))\nScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName\n\nexport { ScrollArea, ScrollBar }\n"
  },
  {
    "path": "src/components/ui/separator.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as SeparatorPrimitive from \"@radix-ui/react-separator\"\n\nimport { cn } from \"~/lib/utils\"\n\nconst Separator = React.forwardRef<\n  React.ElementRef<typeof SeparatorPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>\n>(\n  (\n    { className, orientation = \"horizontal\", decorative = true, ...props },\n    ref\n  ) => (\n    <SeparatorPrimitive.Root\n      ref={ref}\n      decorative={decorative}\n      orientation={orientation}\n      className={cn(\n        \"shrink-0 bg-border\",\n        orientation === \"horizontal\" ? \"h-[1px] w-full\" : \"h-full w-[1px]\",\n        className\n      )}\n      {...props}\n    />\n  )\n)\nSeparator.displayName = SeparatorPrimitive.Root.displayName\n\nexport { Separator }\n"
  },
  {
    "path": "src/components/ui/skeleton.tsx",
    "content": "import { cn } from \"src/lib/utils\"\n\nfunction Skeleton({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) {\n  return (\n    <div\n      className={cn(\"animate-pulse rounded-md bg-muted\", className)}\n      {...props}\n    />\n  )\n}\n\nexport { Skeleton }\n"
  },
  {
    "path": "src/components/ui/toast.tsx",
    "content": "import * as React from \"react\"\nimport * as ToastPrimitives from \"@radix-ui/react-toast\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { X } from \"lucide-react\"\n\nimport { cn } from \"~/lib/utils\"\n\nconst ToastProvider = ToastPrimitives.Provider\n\nconst ToastViewport = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Viewport>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Viewport\n    ref={ref}\n    className={cn(\n      \"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]\",\n      className\n    )}\n    {...props}\n  />\n))\nToastViewport.displayName = ToastPrimitives.Viewport.displayName\n\nconst toastVariants = cva(\n  \"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full\",\n  {\n    variants: {\n      variant: {\n        default: \"border bg-background\",\n        destructive:\n          \"destructive group border-destructive bg-destructive text-destructive-foreground\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  }\n)\n\nconst Toast = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Root>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &\n    VariantProps<typeof toastVariants>\n>(({ className, variant, ...props }, ref) => {\n  return (\n    <ToastPrimitives.Root\n      ref={ref}\n      className={cn(toastVariants({ variant }), className)}\n      {...props}\n    />\n  )\n})\nToast.displayName = ToastPrimitives.Root.displayName\n\nconst ToastAction = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Action>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Action\n    ref={ref}\n    className={cn(\n      \"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive\",\n      className\n    )}\n    {...props}\n  />\n))\nToastAction.displayName = ToastPrimitives.Action.displayName\n\nconst ToastClose = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Close>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Close\n    ref={ref}\n    className={cn(\n      \"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600\",\n      className\n    )}\n    toast-close=\"\"\n    {...props}\n  >\n    <X className=\"h-4 w-4\" />\n  </ToastPrimitives.Close>\n))\nToastClose.displayName = ToastPrimitives.Close.displayName\n\nconst ToastTitle = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Title>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Title\n    ref={ref}\n    className={cn(\"text-sm font-semibold\", className)}\n    {...props}\n  />\n))\nToastTitle.displayName = ToastPrimitives.Title.displayName\n\nconst ToastDescription = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Description>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Description\n    ref={ref}\n    className={cn(\"text-sm opacity-90\", className)}\n    {...props}\n  />\n))\nToastDescription.displayName = ToastPrimitives.Description.displayName\n\ntype ToastProps = React.ComponentPropsWithoutRef<typeof Toast>\n\ntype ToastActionElement = React.ReactElement<typeof ToastAction>\n\nexport {\n  type ToastProps,\n  type ToastActionElement,\n  ToastProvider,\n  ToastViewport,\n  Toast,\n  ToastTitle,\n  ToastDescription,\n  ToastClose,\n  ToastAction,\n}\n"
  },
  {
    "path": "src/components/ui/toaster.tsx",
    "content": "\"use client\"\n\nimport {\n  Toast,\n  ToastClose,\n  ToastDescription,\n  ToastProvider,\n  ToastTitle,\n  ToastViewport,\n} from \"~/components/ui/toast\"\nimport { useToast } from \"~/components/ui/use-toast\"\n\nexport function Toaster() {\n  const { toasts } = useToast()\n\n  return (\n    <ToastProvider>\n      {toasts.map(function ({ id, title, description, action, ...props }) {\n        return (\n          <Toast key={id} {...props}>\n            <div className=\"grid gap-1\">\n              {title && <ToastTitle>{title}</ToastTitle>}\n              {description && (\n                <ToastDescription>{description}</ToastDescription>\n              )}\n            </div>\n            {action}\n            <ToastClose />\n          </Toast>\n        )\n      })}\n      <ToastViewport />\n    </ToastProvider>\n  )\n}\n"
  },
  {
    "path": "src/components/ui/use-toast.ts",
    "content": "// Inspired by react-hot-toast library\nimport * as React from \"react\"\n\nimport type {\n  ToastActionElement,\n  ToastProps,\n} from \"~/components/ui/toast\"\n\nconst TOAST_LIMIT = 1\nconst TOAST_REMOVE_DELAY = 1000000\n\ntype ToasterToast = ToastProps & {\n  id: string\n  title?: React.ReactNode\n  description?: React.ReactNode\n  action?: ToastActionElement\n}\n\nconst actionTypes = {\n  ADD_TOAST: \"ADD_TOAST\",\n  UPDATE_TOAST: \"UPDATE_TOAST\",\n  DISMISS_TOAST: \"DISMISS_TOAST\",\n  REMOVE_TOAST: \"REMOVE_TOAST\",\n} as const\n\nlet count = 0\n\nfunction genId() {\n  count = (count + 1) % Number.MAX_VALUE\n  return count.toString()\n}\n\ntype ActionType = typeof actionTypes\n\ntype Action =\n  | {\n      type: ActionType[\"ADD_TOAST\"]\n      toast: ToasterToast\n    }\n  | {\n      type: ActionType[\"UPDATE_TOAST\"]\n      toast: Partial<ToasterToast>\n    }\n  | {\n      type: ActionType[\"DISMISS_TOAST\"]\n      toastId?: ToasterToast[\"id\"]\n    }\n  | {\n      type: ActionType[\"REMOVE_TOAST\"]\n      toastId?: ToasterToast[\"id\"]\n    }\n\ninterface State {\n  toasts: ToasterToast[]\n}\n\nconst toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()\n\nconst addToRemoveQueue = (toastId: string) => {\n  if (toastTimeouts.has(toastId)) {\n    return\n  }\n\n  const timeout = setTimeout(() => {\n    toastTimeouts.delete(toastId)\n    dispatch({\n      type: \"REMOVE_TOAST\",\n      toastId: toastId,\n    })\n  }, TOAST_REMOVE_DELAY)\n\n  toastTimeouts.set(toastId, timeout)\n}\n\nexport const reducer = (state: State, action: Action): State => {\n  switch (action.type) {\n    case \"ADD_TOAST\":\n      return {\n        ...state,\n        toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),\n      }\n\n    case \"UPDATE_TOAST\":\n      return {\n        ...state,\n        toasts: state.toasts.map((t) =>\n          t.id === action.toast.id ? { ...t, ...action.toast } : t\n        ),\n      }\n\n    case \"DISMISS_TOAST\": {\n      const { toastId } = action\n\n      // ! Side effects ! - This could be extracted into a dismissToast() action,\n      // but I'll keep it here for simplicity\n      if (toastId) {\n        addToRemoveQueue(toastId)\n      } else {\n        state.toasts.forEach((toast) => {\n          addToRemoveQueue(toast.id)\n        })\n      }\n\n      return {\n        ...state,\n        toasts: state.toasts.map((t) =>\n          t.id === toastId || toastId === undefined\n            ? {\n                ...t,\n                open: false,\n              }\n            : t\n        ),\n      }\n    }\n    case \"REMOVE_TOAST\":\n      if (action.toastId === undefined) {\n        return {\n          ...state,\n          toasts: [],\n        }\n      }\n      return {\n        ...state,\n        toasts: state.toasts.filter((t) => t.id !== action.toastId),\n      }\n  }\n}\n\nconst listeners: Array<(state: State) => void> = []\n\nlet memoryState: State = { toasts: [] }\n\nfunction dispatch(action: Action) {\n  memoryState = reducer(memoryState, action)\n  listeners.forEach((listener) => {\n    listener(memoryState)\n  })\n}\n\ntype Toast = Omit<ToasterToast, \"id\">\n\nfunction toast({ ...props }: Toast) {\n  const id = genId()\n\n  const update = (props: ToasterToast) =>\n    dispatch({\n      type: \"UPDATE_TOAST\",\n      toast: { ...props, id },\n    })\n  const dismiss = () => dispatch({ type: \"DISMISS_TOAST\", toastId: id })\n\n  dispatch({\n    type: \"ADD_TOAST\",\n    toast: {\n      ...props,\n      id,\n      open: true,\n      onOpenChange: (open) => {\n        if (!open) dismiss()\n      },\n    },\n  })\n\n  return {\n    id: id,\n    dismiss,\n    update,\n  }\n}\n\nfunction useToast() {\n  const [state, setState] = React.useState<State>(memoryState)\n\n  React.useEffect(() => {\n    listeners.push(setState)\n    return () => {\n      const index = listeners.indexOf(setState)\n      if (index > -1) {\n        listeners.splice(index, 1)\n      }\n    }\n  }, [state])\n\n  return {\n    ...state,\n    toast,\n    dismiss: (toastId?: string) => dispatch({ type: \"DISMISS_TOAST\", toastId }),\n  }\n}\n\nexport { useToast, toast }\n"
  },
  {
    "path": "src/db/client.ts",
    "content": "import { env } from \"~/env.mjs\"\nimport * as schema from \"./schema\"\nimport { neon, neonConfig } from \"@neondatabase/serverless\"\nimport { drizzle } from \"drizzle-orm/neon-http\"\n\nneonConfig.fetchConnectionCache = true\n\nconst sql = neon(env.DATABASE_URL)\nexport const db = drizzle(sql, { schema })\n"
  },
  {
    "path": "src/db/migrate.ts",
    "content": "import { drizzle, type PostgresJsDatabase } from \"drizzle-orm/postgres-js\"\nimport { migrate } from \"drizzle-orm/postgres-js/migrator\"\nimport postgres from \"postgres\"\nimport \"dotenv/config\"\n\nconst sql = postgres(process.env.DATABASE_URL!, {\n  max: 1,\n})\nconst db: PostgresJsDatabase = drizzle(sql)\n\nawait migrate(db, { migrationsFolder: \"drizzle\" })\nconsole.log(\"migration completed\")\n"
  },
  {
    "path": "src/db/schema.ts",
    "content": "import { relations } from \"drizzle-orm\"\nimport {\n  pgTable,\n  varchar,\n  integer,\n  timestamp,\n  pgEnum,\n  primaryKey,\n  uniqueIndex,\n  index,\n  unique,\n} from \"drizzle-orm/pg-core\"\nimport { planTuple } from \"~/lib/configs\"\n\nexport const membershipEnum = pgEnum(\"membership\", planTuple)\nexport const accounts = pgTable(\n  \"accounts\",\n  {\n    id: varchar(\"id\", { length: 256 }).primaryKey(),\n    createdAt: timestamp(\"createdAt\").defaultNow().notNull(),\n    email: varchar(\"email\", { length: 256 }).notNull(),\n    membership: membershipEnum(\"membership\").notNull().default(\"free\"),\n    stripeCustomerId: varchar(\"stripe_customer_id\", { length: 256 }),\n    activeProfileId: varchar(\"active_profile_id\", { length: 256 }).notNull(),\n  },\n  (table) => {\n    return {\n      activeProfileIdx: uniqueIndex(\"active_profile_idx\").on(\n        table.activeProfileId,\n      ),\n    }\n  },\n)\nexport const accountsRelations = relations(accounts, ({ many, one }) => ({\n  profiles: many(profiles),\n  activeProfile: one(profiles, {\n    fields: [accounts.activeProfileId],\n    references: [profiles.id],\n  }),\n}))\n\nexport const profiles = pgTable(\n  \"profiles\",\n  {\n    id: varchar(\"id\", { length: 256 }).primaryKey(),\n    accountId: varchar(\"account_id\", { length: 256 })\n      .references(() => accounts.id, { onDelete: \"cascade\" })\n      .notNull(),\n    profileImgPath: varchar(\"profile_img_path\", { length: 256 }).notNull(),\n    name: varchar(\"name\", { length: 256 }).notNull(),\n  },\n  (table) => {\n    return {\n      unq: unique().on(table.accountId, table.name),\n      accountIdIdx: index(\"account_id_idx\").on(table.accountId),\n    }\n  },\n)\nexport const profilesRelation = relations(profiles, ({ one, many }) => ({\n  ownerAccount: one(accounts, {\n    fields: [profiles.accountId],\n    references: [accounts.id],\n  }),\n  savedShows: many(myShows),\n}))\n\nexport const mediaTypeEnum = pgEnum(\"media_type\", [\"movie\", \"tv\"])\nexport const myShows = pgTable(\n  \"my_shows\",\n  {\n    id: integer(\"id\").notNull(),\n    mediaType: mediaTypeEnum(\"media_type\").notNull(),\n    profileId: varchar(\"profile_id\", { length: 256 })\n      .references(() => profiles.id, { onDelete: \"cascade\" })\n      .notNull(),\n  },\n  (table) => {\n    return {\n      profileIdIdx: index(\"profile_id_idx\").on(table.profileId),\n      pk: primaryKey(table.id, table.profileId),\n    }\n  },\n)\nexport const myShowsRelation = relations(myShows, ({ one }) => ({\n  profile: one(profiles, {\n    fields: [myShows.profileId],\n    references: [profiles.id],\n  }),\n}))\n"
  },
  {
    "path": "src/env.mjs",
    "content": "import { createEnv } from \"@t3-oss/env-nextjs\"\nimport { z } from \"zod\"\n\nexport const env = createEnv({\n  /**\n   * Specify your server-side environment variables schema here. This way you can ensure the app\n   * isn't built with invalid env vars.\n   */\n  server: {\n    NODE_ENV: z.enum([\"development\", \"test\", \"production\"]),\n\n    DATABASE_URL: z.string().url(),\n    CLERK_SECRET_KEY: z.string().min(1),\n    STRIPE_SECRET_KEY: z.string().min(1),\n    STRIPE_WEBHOOK_SECRET: z.string().min(1),\n    STRIPE_DEV_WEBHOOK_SECRET: z.string().min(1),\n  },\n\n  /**\n   * Specify your client-side environment variables schema here. This way you can ensure the app\n   * isn't built with invalid env vars. To expose them to the client, prefix them with\n   * `NEXT_PUBLIC_`.\n   */\n  client: {\n    // NEXT_PUBLIC_CLIENTVAR: z.string().min(1),\n\n    NEXT_PUBLIC_TMDB_API: z.string().min(1),\n    NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string().min(1),\n    NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string().min(1),\n  },\n\n  /**\n   * You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g.\n   * middlewares) or client-side so we need to destruct manually.\n   */\n  runtimeEnv: {\n    NODE_ENV: process.env.NODE_ENV,\n    // NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,\n\n    DATABASE_URL: process.env.DATABASE_URL,\n    NEXT_PUBLIC_TMDB_API: process.env.NEXT_PUBLIC_TMDB_API,\n    NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY:\n      process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY,\n    CLERK_SECRET_KEY: process.env.CLERK_SECRET_KEY,\n    NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY:\n      process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY,\n    STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY,\n    STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET,\n    STRIPE_DEV_WEBHOOK_SECRET: process.env.STRIPE_DEV_WEBHOOK_SECRET,\n  },\n  /**\n   * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation.\n   * This is especially useful for Docker builds.\n   */\n  skipValidation: !!process.env.SKIP_ENV_VALIDATION,\n})\n"
  },
  {
    "path": "src/lib/client-fetchers.ts",
    "content": "import { env } from \"~/env.mjs\"\nimport type { Show, MediaType } from \"./types\"\nimport { ERR } from \"./utils\"\n\nexport async function getShows(mediaType: MediaType) {\n  const [\n    trendingRes,\n    topRatedRes,\n    actionThrillerRes,\n    comedyRes,\n    horrorRes,\n    romanceRes,\n    documentaryRes,\n  ] = await Promise.all([\n    fetch(\n      `https://api.themoviedb.org/3/trending/${mediaType}/week?api_key=${env.NEXT_PUBLIC_TMDB_API}`,\n    ),\n    fetch(\n      `https://api.themoviedb.org/3/${mediaType}/top_rated?api_key=${env.NEXT_PUBLIC_TMDB_API}`,\n    ),\n    fetch(\n      `https://api.themoviedb.org/3/discover/${mediaType}?api_key=${env.NEXT_PUBLIC_TMDB_API}&with_genres=28`,\n    ),\n    fetch(\n      `https://api.themoviedb.org/3/discover/${mediaType}?api_key=${env.NEXT_PUBLIC_TMDB_API}&with_genres=35`,\n    ),\n    fetch(\n      `https://api.themoviedb.org/3/discover/${mediaType}?api_key=${env.NEXT_PUBLIC_TMDB_API}&with_genres=27`,\n    ),\n    fetch(\n      `https://api.themoviedb.org/3/discover/${mediaType}?api_key=${env.NEXT_PUBLIC_TMDB_API}&with_genres=10749`,\n    ),\n    fetch(\n      `https://api.themoviedb.org/3/discover/${mediaType}?api_key=${env.NEXT_PUBLIC_TMDB_API}&with_genres=99`,\n    ),\n  ])\n\n  if (\n    !trendingRes.ok ||\n    !topRatedRes.ok ||\n    !actionThrillerRes.ok ||\n    !comedyRes.ok ||\n    !horrorRes.ok ||\n    !romanceRes.ok ||\n    !documentaryRes.ok\n  )\n    throw new Error(ERR.fetch)\n\n  const [\n    trending,\n    topRated,\n    actionThriller,\n    comedy,\n    horror,\n    romance,\n    documentary,\n  ] = await Promise.all<{ results: Show[] }>([\n    trendingRes.json(),\n    topRatedRes.json(),\n    actionThrillerRes.json(),\n    comedyRes.json(),\n    horrorRes.json(),\n    romanceRes.json(),\n    documentaryRes.json(),\n  ])\n\n  if (\n    !trending ||\n    !topRated ||\n    !actionThriller ||\n    !comedy ||\n    !horror ||\n    !romance ||\n    !documentary\n  )\n    throw new Error(ERR.fetch)\n\n  return {\n    trending: trending.results,\n    topRated: topRated.results,\n    actionThriller: actionThriller.results,\n    comedy: comedy.results,\n    horror: horror.results,\n    romance: romance.results,\n    documentary: documentary.results,\n  }\n}\n"
  },
  {
    "path": "src/lib/configs.ts",
    "content": "export const PLANS = [\n  {\n    id: \"0\",\n    name: \"free\",\n    price: 0,\n    description: \"Free Video Stream\",\n  },\n  {\n    id: \"price_1NeEIZBrDYSkolG5bjbEmccv\",\n    name: \"basic\",\n    price: 5,\n    description: \"Basic Video Stream\",\n  },\n  {\n    id: \"price_1NeE60BrDYSkolG5PidpmNHW\",\n    name: \"standard\",\n    price: 10,\n    description: \"Standard Video Stream\",\n  },\n  {\n    id: \"price_1Nd71XBrDYSkolG53ADLYQXk\",\n    name: \"premium\",\n    price: 20,\n    description: \"Premium Video Stream\",\n  },\n] as const\n\nconst tupleMap = <\n  const T extends readonly Record<string, unknown>[],\n  Key extends keyof T[number],\n>(\n  input: T,\n  key: Key,\n): { [k in keyof T]: T[k][Key] } => {\n  return input.map((row) => row[key as never]) as never\n}\nexport const planTuple = tupleMap(PLANS, \"name\")\n"
  },
  {
    "path": "src/lib/globals.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer base {\n  :root {\n    --background: 0 0% 100%;\n    --foreground: 0 0% 3.9%;\n\n    --muted: 0 0% 96.1%;\n    --muted-foreground: 0 0% 45.1%;\n\n    --popover: 0 0% 100%;\n    --popover-foreground: 0 0% 3.9%;\n\n    --card: 0 0% 100%;\n    --card-foreground: 0 0% 3.9%;\n\n    --border: 0 0% 89.8%;\n    --input: 0 0% 89.8%;\n\n    --primary: 0 0% 9%;\n    --primary-foreground: 0 0% 98%;\n\n    --secondary: 0 0% 96.1%;\n    --secondary-foreground: 0 0% 9%;\n\n    --accent: 0 0% 96.1%;\n    --accent-foreground: 0 0% 9%;\n\n    --destructive: 0 84.2% 60.2%;\n    --destructive-foreground: 0 0% 98%;\n\n    --ring: 0 0% 63.9%;\n\n    --radius: 0.5rem;\n  }\n\n  .dark {\n    --background: 0 0% 3.9%;\n    --foreground: 0 0% 98%;\n\n    --muted: 0 0% 14.9%;\n    --muted-foreground: 0 0% 63.9%;\n\n    --popover: 0 0% 3.9%;\n    --popover-foreground: 0 0% 98%;\n\n    --card: 0 0% 3.9%;\n    --card-foreground: 0 0% 98%;\n\n    --border: 0 0% 14.9%;\n    --input: 0 0% 14.9%;\n\n    --primary: 0 0% 98%;\n    --primary-foreground: 0 0% 9%;\n\n    --secondary: 0 0% 14.9%;\n    --secondary-foreground: 0 0% 98%;\n\n    --accent: 0 0% 14.9%;\n    --accent-foreground: 0 0% 98%;\n\n    --destructive: 0 62.8% 30.6%;\n    --destructive-foreground: 0 85.7% 97.3%;\n\n    --ring: 0 0% 14.9%;\n  }\n}\n\n@layer base {\n  * {\n    @apply border-border;\n  }\n  body {\n    @apply bg-background text-foreground;\n  }\n}\n"
  },
  {
    "path": "src/lib/server-fetchers.ts",
    "content": "import { env } from \"~/env.mjs\"\nimport type { Show, MyShow, ShowWithVideoAndGenre } from \"~/lib/types\"\nimport { ERR } from \"~/lib/utils\"\nimport { db } from \"~/db/client\"\nimport { eq } from \"drizzle-orm\"\nimport { accounts, profiles, myShows } from \"~/db/schema\"\nimport { auth } from \"@clerk/nextjs\"\nimport type { MediaType } from \"~/lib/types\"\n\nexport async function getAccount() {\n  const userId = auth().userId\n  if (!userId) throw new Error(ERR.unauthenticated)\n  const account = await db.query.accounts.findFirst({\n    where: eq(accounts.id, userId),\n  })\n  if (!account) throw new Error(ERR.db)\n  return account\n}\n\nexport async function getAccountWithActiveProfile() {\n  const userId = auth().userId\n  if (!userId) throw new Error(ERR.unauthenticated)\n  const account = await db.query.accounts.findFirst({\n    where: eq(accounts.id, userId),\n    columns: { activeProfileId: true },\n    with: {\n      activeProfile: true,\n    },\n  })\n  if (!account) throw new Error(ERR.db)\n  return account\n}\n\nexport async function getAccountWithProfiles() {\n  const userId = auth().userId\n  if (!userId) throw new Error(ERR.unauthenticated)\n  const account = await db.query.accounts.findFirst({\n    where: eq(accounts.id, userId),\n    with: {\n      profiles: true,\n    },\n  })\n  if (!account) throw new Error(ERR.db)\n  return account\n}\n\nexport async function getProfile(profileId: string) {\n  const profile = await db.query.profiles.findFirst({\n    where: eq(profiles.id, profileId),\n  })\n  if (!profile) throw new Error(ERR.db)\n  return profile\n}\n\nexport async function getMyShows(limit: number) {\n  const account = await getAccountWithActiveProfile()\n  const shows = await db.query.myShows.findMany({\n    where: eq(myShows.profileId, account.activeProfileId),\n    limit: limit + 1,\n  })\n  const hasNextPage = shows.length > limit ? true : false\n  if (hasNextPage) shows.pop()\n  const filteredShows = await getMyShowsFromTmdb(shows)\n  return { shows: filteredShows, hasNextPage }\n}\n\nexport async function getMyShowsFromTmdb(shows: MyShow[]) {\n  const data = await Promise.all<Show | null>(\n    shows.map(async (show) => {\n      const res = await fetch(\n        `https://api.themoviedb.org/3/${show.mediaType}/${show.id}?api_key=${env.NEXT_PUBLIC_TMDB_API}`,\n      )\n      if (!res.ok) return null\n      return res.json()\n    }),\n  )\n  const filteredShows = data.filter((el): el is Show => !!el)\n  return filteredShows\n}\n\nexport async function getShowVideoAndGenreWithStatus(\n  showId: number,\n  mediaType: MediaType,\n) {\n  const userId = auth().userId\n  const accountPromise = userId\n    ? db.query.accounts.findFirst({\n        where: eq(accounts.id, userId),\n        columns: {},\n        with: {\n          activeProfile: {\n            columns: {},\n            with: {\n              savedShows: {\n                where: eq(myShows.id, showId),\n                limit: 1,\n              },\n            },\n          },\n        },\n      })\n    : null\n  const [show, account] = await Promise.all([\n    fetch(\n      `https://api.themoviedb.org/3/${mediaType}/${showId}?api_key=${env.NEXT_PUBLIC_TMDB_API}&append_to_response=videos,genres`,\n    )\n      .then((r) => r.json() as Promise<ShowWithVideoAndGenre>)\n      .catch((err) => console.error(err)),\n    accountPromise,\n  ])\n  if (!show || account === undefined) throw new Error(ERR.fetch)\n  const isSaved = account\n    ? !!account.activeProfile.savedShows.length\n    : undefined\n\n  return { show, isSaved }\n}\n"
  },
  {
    "path": "src/lib/stripe.ts",
    "content": "import Stripe from \"stripe\"\nimport { env } from \"~/env.mjs\"\n\nexport const stripe = new Stripe(env.STRIPE_SECRET_KEY, {\n  apiVersion: \"2023-08-16\",\n  typescript: true,\n})\n"
  },
  {
    "path": "src/lib/types.ts",
    "content": "import { type myShows, type profiles, mediaTypeEnum } from \"~/db/schema\"\nimport type { PLANS } from \"~/lib/configs\"\n\nexport type MyShow = typeof myShows.$inferSelect\nexport const MediaTuple = mediaTypeEnum.enumValues\nexport type MediaType = (typeof MediaTuple)[number]\nexport type Profile = typeof profiles.$inferSelect\nexport type SubscriptionPlan = (typeof PLANS)[number]\nexport type PlanName = (typeof PLANS)[number][\"name\"]\n\nexport interface Show {\n  id: number\n  backdrop_path: string\n  poster_path: string\n  title?: string\n  name?: string\n  overview: string\n  vote_average: number\n  popularity: number\n  release_date?: string\n  first_air_date?: string\n}\nexport interface ShowWithVideoAndGenre extends Show {\n  videos: {\n    results: Video[]\n  }\n  genres: Genre[]\n}\n\ntype Video = {\n  key: string\n  type: string\n}\n\ntype Genre = {\n  id: number\n  name: string\n}\n\n// \"adult\": false,\n// \"backdrop_path\": \"/e2Jd0sYMCe6qvMbswGQbM0Mzxt0.jpg\",\n// \"genre_ids\": [\n//   28,\n//   80,\n//   53\n// ],\n// \"id\": 385687,\n// \"original_language\": \"en\",\n// \"original_title\": \"Fast X\",\n// \"overview\": \"Over many missions and against impossible odds, Dom Toretto and his family have outsmarted, out-nerved and outdriven every foe in their path. Now, they confront the most lethal opponent they've ever faced: A terrifying threat emerging from the shadows of the past who's fueled by blood revenge, and who is determined to shatter this family and destroy everything—and everyone—that Dom loves, forever.\",\n// \"popularity\": 4654.279,\n// \"poster_path\": \"/fiVW06jE7z9YnO4trhaMEdclSiC.jpg\",\n// \"release_date\": \"2023-05-17\",\n// \"title\": \"Fast X\",\n// \"video\": false,\n// \"vote_average\": 7.3,\n// \"vote_count\": 2093\n"
  },
  {
    "path": "src/lib/utils.ts",
    "content": "import { type ClassValue, clsx } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\nimport type { Show } from \"./types\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n\nexport const ERR = {\n  unauthenticated: \"Unauthenticated\",\n  unauthorized: \"Unauthorized\",\n  db: \"Failed to find in database\",\n  undefined: \"Undefined variable\",\n  fetch: \"Failed to fetch data\",\n  not_allowed: \"User should not be allowed to do this action\",\n}\n\nexport function pickRandomShow(shows: Show[]) {\n  const show = shows[Math.floor(Math.random() * shows.length)]\n  if (show) return show\n  else throw new Error(ERR.undefined)\n}\n"
  },
  {
    "path": "src/middleware.ts",
    "content": "import { authMiddleware } from \"@clerk/nextjs\"\nexport default authMiddleware({\n  publicRoutes: [\n    \"/\",\n    \"/api/(.*)\",\n    \"/show/(.*)\",\n    \"/tv-shows\",\n    \"/movies\",\n    \"/new-and-popular\",\n    \"/search(.*)\",\n  ],\n})\n\nexport const config = {\n  matcher: [\"/((?!.*\\\\..*|_next).*)\", \"/\", \"/(api|trpc)(.*)\"],\n}\n"
  },
  {
    "path": "tailwind.config.ts",
    "content": "/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n  darkMode: [\"class\"],\n  content: [\n    \"./pages/**/*.{ts,tsx}\",\n    \"./components/**/*.{ts,tsx}\",\n    \"./app/**/*.{ts,tsx}\",\n    \"./src/**/*.{ts,tsx}\",\n  ],\n  theme: {\n    container: {\n      center: true,\n      padding: \"2rem\",\n      screens: {\n        \"2xl\": \"1400px\",\n      },\n    },\n    extend: {\n      colors: {\n        border: \"hsl(var(--border))\",\n        input: \"hsl(var(--input))\",\n        ring: \"hsl(var(--ring))\",\n        background: \"hsl(var(--background))\",\n        foreground: \"hsl(var(--foreground))\",\n        primary: {\n          DEFAULT: \"hsl(var(--primary))\",\n          foreground: \"hsl(var(--primary-foreground))\",\n        },\n        secondary: {\n          DEFAULT: \"hsl(var(--secondary))\",\n          foreground: \"hsl(var(--secondary-foreground))\",\n        },\n        destructive: {\n          DEFAULT: \"hsl(var(--destructive))\",\n          foreground: \"hsl(var(--destructive-foreground))\",\n        },\n        muted: {\n          DEFAULT: \"hsl(var(--muted))\",\n          foreground: \"hsl(var(--muted-foreground))\",\n        },\n        accent: {\n          DEFAULT: \"hsl(var(--accent))\",\n          foreground: \"hsl(var(--accent-foreground))\",\n        },\n        popover: {\n          DEFAULT: \"hsl(var(--popover))\",\n          foreground: \"hsl(var(--popover-foreground))\",\n        },\n        card: {\n          DEFAULT: \"hsl(var(--card))\",\n          foreground: \"hsl(var(--card-foreground))\",\n        },\n      },\n      borderRadius: {\n        lg: \"var(--radius)\",\n        md: \"calc(var(--radius) - 2px)\",\n        sm: \"calc(var(--radius) - 4px)\",\n      },\n      keyframes: {\n        \"accordion-down\": {\n          from: { height: 0 },\n          to: { height: \"var(--radix-accordion-content-height)\" },\n        },\n        \"accordion-up\": {\n          from: { height: \"var(--radix-accordion-content-height)\" },\n          to: { height: 0 },\n        },\n      },\n      animation: {\n        \"accordion-down\": \"accordion-down 0.2s ease-out\",\n        \"accordion-up\": \"accordion-up 0.2s ease-out\",\n      },\n      screens: {\n        mobile: { raw: \"(pointer: coarse)\" },\n      },\n    },\n  },\n  plugins: [\n    require(\"tailwindcss-animate\"),\n    // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-var-requires\n    require(\"tailwind-scrollbar\")({ nocompatible: true }),\n  ],\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es2017\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"checkJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"incremental\": true,\n    \"noUncheckedIndexedAccess\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"~/*\": [\"./src/*\"],\n      \"@/*\": [\"./*\"]\n    },\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ]\n  },\n  \"include\": [\n    \".eslintrc.cjs\",\n    \"next-env.d.ts\",\n    \"**/*.ts\",\n    \"**/*.tsx\",\n    \"**/*.cjs\",\n    \"**/*.mjs\",\n    \".next/types/**/*.ts\"\n  ],\n  \"exclude\": [\"node_modules\"]\n}\n"
  }
]