Full Code of Apestein/nextflix for AI

main 0eb4be5f1bd8 cached
81 files
126.4 KB
35.9k tokens
96 symbols
1 requests
Download .txt
Repository: Apestein/nextflix
Branch: main
Commit: 0eb4be5f1bd8
Files: 81
Total size: 126.4 KB

Directory structure:
gitextract_w94r5yd9/

├── .eslintrc.cjs
├── .github/
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── README.md
├── components.json
├── drizzle.config.ts
├── next.config.mjs
├── package.json
├── postcss.config.cjs
├── prettier.config.cjs
├── src/
│   ├── actions/
│   │   ├── index.ts
│   │   └── safe-action-client.ts
│   ├── app/
│   │   ├── (auth)/
│   │   │   ├── layout.tsx
│   │   │   ├── sign-in/
│   │   │   │   └── [[...sign-in]]/
│   │   │   │       └── page.tsx
│   │   │   └── sign-up/
│   │   │       └── [[...sign-up]]/
│   │   │           └── page.tsx
│   │   ├── (main)/
│   │   │   ├── @modal/
│   │   │   │   ├── (.)show/
│   │   │   │   │   └── [id]/
│   │   │   │   │       ├── modal.tsx
│   │   │   │   │       └── page.tsx
│   │   │   │   ├── default.tsx
│   │   │   │   └── loading.tsx
│   │   │   ├── account/
│   │   │   │   ├── loading.tsx
│   │   │   │   └── page.tsx
│   │   │   ├── default.tsx
│   │   │   ├── layout.tsx
│   │   │   ├── loading.tsx
│   │   │   ├── movies/
│   │   │   │   └── page.tsx
│   │   │   ├── my-list/
│   │   │   │   ├── infinite-scroller.tsx
│   │   │   │   ├── loading.tsx
│   │   │   │   └── page.tsx
│   │   │   ├── new-and-popular/
│   │   │   │   └── page.tsx
│   │   │   ├── page.tsx
│   │   │   ├── search/
│   │   │   │   ├── loading.tsx
│   │   │   │   ├── page.tsx
│   │   │   │   └── search-input.tsx
│   │   │   ├── show/
│   │   │   │   └── [id]/
│   │   │   │       └── page.tsx
│   │   │   ├── subscription/
│   │   │   │   ├── loading.tsx
│   │   │   │   ├── page.tsx
│   │   │   │   ├── plan-selector.tsx
│   │   │   │   └── result/
│   │   │   │       └── page.tsx
│   │   │   └── tv-shows/
│   │   │       └── page.tsx
│   │   ├── (profile)/
│   │   │   ├── loading.tsx
│   │   │   ├── manage-profile/
│   │   │   │   ├── [...slug]/
│   │   │   │   │   └── page.tsx
│   │   │   │   ├── add/
│   │   │   │   │   └── page.tsx
│   │   │   │   └── page.tsx
│   │   │   └── switch-profile/
│   │   │       ├── page.tsx
│   │   │       └── profile-switcher.tsx
│   │   ├── api/
│   │   │   └── (webhook)/
│   │   │       └── stripe/
│   │   │           └── route.ts
│   │   ├── error.tsx
│   │   ├── layout.tsx
│   │   └── not-found.tsx
│   ├── components/
│   │   ├── link-button.tsx
│   │   ├── modal-card.tsx
│   │   ├── overlay-scrollbar.tsx
│   │   ├── show-bg.tsx
│   │   ├── show-carousel.tsx
│   │   ├── show-hero.tsx
│   │   ├── theme-provider.tsx
│   │   └── ui/
│   │       ├── button.tsx
│   │       ├── card.tsx
│   │       ├── dropdown-menu.tsx
│   │       ├── input.tsx
│   │       ├── label.tsx
│   │       ├── scroll-area.tsx
│   │       ├── separator.tsx
│   │       ├── skeleton.tsx
│   │       ├── toast.tsx
│   │       ├── toaster.tsx
│   │       └── use-toast.ts
│   ├── db/
│   │   ├── client.ts
│   │   ├── migrate.ts
│   │   └── schema.ts
│   ├── env.mjs
│   ├── lib/
│   │   ├── client-fetchers.ts
│   │   ├── configs.ts
│   │   ├── globals.css
│   │   ├── server-fetchers.ts
│   │   ├── stripe.ts
│   │   ├── types.ts
│   │   └── utils.ts
│   └── middleware.ts
├── tailwind.config.ts
└── tsconfig.json

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

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

    "@typescript-eslint/consistent-type-imports": [
      "warn",
      {
        prefer: "type-imports",
        fixStyle: "inline-type-imports",
      },
    ],
    "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
  },
}

module.exports = config


================================================
FILE: .github/workflows/ci.yml
================================================
name: CI

on: [push, pull_request]

env:
  DATABASE_URL: "https://fake.com"
  SKIP_ENV_VALIDATION: true

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Bun
        uses: oven-sh/setup-bun@v1

      - name: Install Dependencies
        run: bun install

      - name: Typecheck
        run: bun run typecheck

      - name: Lint
        run: bun run lint

      - name: Print Environment Variable
        run: echo $MY_ENV_VAR


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

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

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

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

# production
/build

# misc
.DS_Store
*.pem

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

# local env files
# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables
.env
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo


================================================
FILE: README.md
================================================
## 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.

### Full Tech Stack

- Next.js 13
- CreateT3App Bootrapped
- Neon (postgres)
- Drizzle ORM
- Tailwind + Shadcn/ui
- Clerk
- Lucide Icons
- Zod Validation
- Stripe

### Project Description

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

### How To Run Locally
Clone 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). 

### Overall Thoughts

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

### Thoughts about Clerk

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

- 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.
  ![Screenshot (83)](https://github.com/Apestein/nextflix/assets/107362680/6d2d89d0-63f3-4d6c-97a7-3a12f514868e)

### Thoughts about Neon

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

### Thoughts about Drizzle

Fantastic. Noticeably faster than Prisma. Schema file being in typescript results in superior DX. Their docs are a little lacking though.

### Project Setup

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

### Project Structure

Some 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.
If 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).
![Screenshot (74)](https://github.com/Apestein/nextflix/assets/107362680/44bffd04-e537-49ca-a945-1b1185a4b64f)

### Tricky Things To Consider (I will be going over things I found tricky or difficult in this section)

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

[scrnli_8_22_2023_12-30-04 PM2.webm](https://github.com/Apestein/nextflix/assets/107362680/da7dd256-0a91-4ce5-99c6-698bc37d8013)

#### 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).

```ts
async function CustomeUserButton() {
  const { userId } = auth()
  if (!userId) return
  const existingAccount = await getAccountWithActiveProfile()
  const account = existingAccount ?? (await createAccountAndProfile())
  ...
}
```

Additionally, you should wrap the component in suspense to not block the UI and prevent unresponsiveness.

```ts
<Suspense fallback={<Skeleton className="h-8 w-8" />}>
  <CustomeUserButton />
</Suspense>
```

#### 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).

```ts
export const createCheckoutSession = authAction(
  z.object({
    stripeProductId: z.string(),
    planName: z.enum(planTuple),
  }),
}
```

![Screenshot (78)](https://github.com/Apestein/nextflix/assets/107362680/98e2f8f8-3b44-46d7-baa6-abe95d8463fa)

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

```ts
// actions/index.ts
export const getMyShowsInfinite = authAction(
  z.object({
    index: z.number().min(0),
    limit: z.number().min(2).max(50),
  }),
  async (input) => {
    const account = await getAccountWithActiveProfile()
    const shows = await db.query.myShows.findMany({
      where: eq(myShows.profileId, account.activeProfileId),
      limit: input.limit + 1,
      offset: input.index * input.limit,
    })
    const hasNextPage = shows.length > input.limit ? true : false
    if (hasNextPage) shows.pop()
    const filteredShows = await getMyShowsFromTmdb(shows)
    return { shows: filteredShows, hasNextPage }
  },
)
```

Then, 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.

```ts
const observer = new IntersectionObserver((entries) => {
  if (!hasNextPageRef.current) return // <= must use ref, don't use state
})
```

[scrnli_8_22_2023_12-42-31 PM3.webm](https://github.com/Apestein/nextflix/assets/107362680/e9ceae54-1ea0-4c89-97c7-0d87d12bd135)

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

- [Official Next.js example using server actions](https://github.com/vercel/next.js/tree/canary/examples/with-stripe-typescript)
- [Taxonomy](https://github.com/shadcn-ui/taxonomy)

#### 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).

[scrnli_8_22_2023_12-17-36 PM.webm](https://github.com/Apestein/nextflix/assets/107362680/00f9690a-8698-498a-b639-5e45b5e5518c)

#### 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🤯.

[scrnli_8_22_2023_12-51-37 PM4.webm](https://github.com/Apestein/nextflix/assets/107362680/3dda2e70-97f5-4d88-beca-1cfda53fc344)

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

https://github.com/Apestein/nextflix/assets/107362680/4136a245-e38f-404a-b66e-2a9c4bc1b266

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

[scrnli_8_25_2023_5-58-40 PM5.webm](https://github.com/Apestein/nextflix/assets/107362680/9e5e7f93-8d1a-493f-8eba-c6dc2b283805)

Moreover, 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:

```ts
//app/layout.tsx
<html
  lang="en"
  suppressHydrationWarning
  className="[&:not(:has([role='dialog'])):has([data-layout='main'])]:[scrollbar-gutter:stable]"
  //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🤯
>
  ...
</html>
```

```ts
//app/(main)/layout.tsx
<div
  className="container flex min-h-screen flex-col px-4 md:px-8"
  data-layout="main" //<= add this
>
  <Header />
  {children}
  <Footer />
</div>
```

Edit: 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.

```ts
//app/layout.tsx
<body
  className={cn(
    "bg-neutral-900 text-slate-50 antialiased [&:has([data-layout='main'])]:overflow-y-scroll",
    inter.className,
  )}
>
  ...
</body>
```
Edit: 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).

#### 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.
```ts
<Link
  href={`/show/${show.id}?mediaType=${
  show.title ? "movie" : "tv"
  }`}
  scroll={false}
  key={show.id}
>
```
If 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>)

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

```ts
export const runtime = "edge"
export const preferredRegion = "iad1"
```

#### 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. 
```ts
<img
  src={`https://image.tmdb.org/t/p/w300${
  show.backdrop_path ?? show.poster_path
}`}
  alt="show-backdrop"
  width={240} //should be on-screen rendered size not image actual resolution
  height={135} //should be on-screen rendered size not image actual resolution
  className="aspect-video min-w-[160px] cursor-pointer object-cover transition-transform hover:scale-110 md:min-w-[240px]"
  />
```

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

#### 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). 

### Follow and ask me questions at [@Apestein_Dev](https://twitter.com/Apestein_Dev).


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

================================================
FILE: drizzle.config.ts
================================================
import type { Config } from "drizzle-kit"
import "dotenv/config"

export default {
  schema: "./src/db/schema.ts",
  out: "./drizzle",
  driver: "pg",
  dbCredentials: {
    connectionString: process.env.DATABASE_URL!,
  },
  schemaFilter: ["public"],
  verbose: true,
  strict: true,
} satisfies Config


================================================
FILE: next.config.mjs
================================================
/**
 * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful
 * for Docker builds.
 */
await import("./src/env.mjs")

/** @type {import("next").NextConfig} */
const config = {
  images: {
    domains: ["image.tmdb.org", "img.clerk.com", "api.dicebear.com"],
  },
  experimental: {
    serverActions: true,
  },
  typescript: {
    ignoreBuildErrors: true,
  },
  eslint: {
    ignoreDuringBuilds: true,
  },
}

export default config


================================================
FILE: package.json
================================================
{
  "name": "nextflix",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "build": "next build",
    "dev": "next dev",
    "lint": "next lint",
    "typecheck": "tsc",
    "start": "next start",
    "introspect": "drizzle-kit introspect:pg",
    "generate": "drizzle-kit generate:pg",
    "migrate": "tsx ./src/db/migrate.ts",
    "push": "drizzle-kit push:pg",
    "studio": "drizzle-kit studio",
    "stripe:listen": "stripe listen --forward-to localhost:3000/api/stripe"
  },
  "dependencies": {
    "@clerk/nextjs": "^4.23.3",
    "@neondatabase/serverless": "^0.6.0",
    "@radix-ui/react-dropdown-menu": "^2.0.5",
    "@radix-ui/react-label": "^2.0.2",
    "@radix-ui/react-scroll-area": "^1.0.4",
    "@radix-ui/react-separator": "^1.0.3",
    "@radix-ui/react-slot": "^1.0.2",
    "@radix-ui/react-toast": "^1.1.4",
    "@t3-oss/env-nextjs": "^0.6.1",
    "@vercel/analytics": "^1.0.2",
    "class-variance-authority": "^0.7.0",
    "clsx": "^2.0.0",
    "dotenv": "^16.3.1",
    "drizzle-orm": "^0.28.5",
    "lucide-react": "^0.274.0",
    "next": "^13.4.19",
    "next-safe-action": "^3.0.1",
    "next-themes": "^0.2.1",
    "overlayscrollbars-react": "^0.5.2",
    "pg": "^8.11.3",
    "postgres": "^3.3.5",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "react-snap-carousel": "^0.3.2",
    "react-use-draggable-scroll": "^0.4.7",
    "stripe": "^13.4.0",
    "tailwind-merge": "^1.14.0",
    "tailwindcss-animate": "^1.0.7",
    "use-debounce": "^9.0.4",
    "zod": "^3.22.2",
    "zod-validation-error": "^1.5.0"
  },
  "devDependencies": {
    "@types/eslint": "^8.44.2",
    "@types/node": "^20.5.9",
    "@types/prettier": "^2.7.3",
    "@types/react": "^18.2.21",
    "@types/react-dom": "^18.2.7",
    "@typescript-eslint/eslint-plugin": "^6.5.0",
    "@typescript-eslint/parser": "^6.5.0",
    "autoprefixer": "^10.4.15",
    "drizzle-kit": "^0.19.13",
    "eslint": "^8.48.0",
    "eslint-config-next": "^13.4.19",
    "postcss": "^8.4.29",
    "prettier": "^3.0.3",
    "prettier-plugin-tailwindcss": "^0.5.4",
    "tailwind-scrollbar": "^3.0.5",
    "tailwindcss": "^3.3.3",
    "tsx": "^3.12.8",
    "typescript": "^5.2.2"
  }
}


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

module.exports = config;


================================================
FILE: prettier.config.cjs
================================================
/** @type {import("prettier").Config} */
const config = {
  plugins: [require.resolve("prettier-plugin-tailwindcss")],
  semi: false,
};

module.exports = config;


================================================
FILE: src/actions/index.ts
================================================
"use server"
import { z } from "zod"
import { authAction } from "./safe-action-client"
import { db } from "~/db/client"
import { eq } from "drizzle-orm"
import { accounts, profiles, myShows } from "~/db/schema"
import { ERR } from "~/lib/utils"
import { revalidatePath } from "next/cache"
import {
  getAccount,
  getAccountWithProfiles,
  getProfile,
  getAccountWithActiveProfile,
  getMyShowsFromTmdb,
} from "~/lib/server-fetchers"
import { stripe } from "~/lib/stripe"
import { headers } from "next/headers"
import { redirect } from "next/navigation"
import type { Stripe } from "stripe"
import { planTuple } from "~/lib/configs"
import { MediaTuple } from "~/lib/types"

export const createProfile = authAction(
  z.object({
    name: z.string().min(2).max(20),
  }),
  async (input, { userId }) => {
    const account = await getAccountWithProfiles()
    if (account.profiles.length === 4) throw new Error(ERR.not_allowed)
    const takenProfileSlots = account.profiles.map((profile) =>
      Number(profile.id.at(-1)),
    )
    const openProfileSlot = [1, 2, 3, 4].find(
      (el) => !takenProfileSlots.includes(el),
    )
    if (!openProfileSlot) throw new Error(ERR.undefined)
    await db.insert(profiles).values({
      id: `${userId}-${openProfileSlot}`,
      accountId: userId,
      name: input.name,
      profileImgPath: `https://api.dicebear.com/6.x/bottts-neutral/svg?seed=${input.name}`,
    })
    revalidatePath("/manage-profile")
    return { message: "Profile Created" }
  },
)

export const deleteProfile = authAction(
  z.object({
    profileId: z.string(),
  }),
  async (input) => {
    const account = await getAccountWithProfiles()
    if (account.activeProfileId === input.profileId)
      return { message: "Cannot delete active profile" }
    if (!account.profiles.find((profile) => profile.id === input.profileId))
      throw new Error(ERR.unauthorized)
    await db.delete(profiles).where(eq(profiles.id, input.profileId))
    revalidatePath("/manage-profile")
    return { message: "Profile Deleted" }
  },
)

export const updateProfile = authAction(
  z.object({
    profileId: z.string(),
    name: z.string().min(2).max(20),
  }),
  async (input, { userId }) => {
    const profile = await getProfile(input.profileId)
    if (userId !== profile.accountId) throw new Error(ERR.unauthorized)
    await db
      .update(profiles)
      .set({
        name: input.name,
        profileImgPath: `https://api.dicebear.com/6.x/bottts-neutral/svg?seed=${input.name}`,
      })
      .where(eq(profiles.id, input.profileId))
    revalidatePath("/manage-profile")
    return { message: "Profile Updated" }
  },
)

export const switchProfile = authAction(
  z.object({
    profileId: z.string(),
  }),
  async (input, { userId }) => {
    const profile = await getProfile(input.profileId)
    if (profile.accountId !== userId) throw new Error(ERR.unauthorized)
    await db
      .update(accounts)
      .set({
        activeProfileId: input.profileId,
      })
      .where(eq(accounts.id, userId))
    revalidatePath("/")
    return { message: "You have switched active profile" }
  },
)

export const toggleMyShow = authAction(
  z.object({
    id: z.number(),
    isSaved: z.boolean(),
    movieOrTv: z.enum(MediaTuple),
  }),
  async (input) => {
    const account = await getAccount()
    if (!input.isSaved) {
      await db.insert(myShows).values({
        id: input.id,
        mediaType: input.movieOrTv,
        profileId: account.activeProfileId,
      })
      return { isSaved: true }
    } else {
      await db.delete(myShows).where(eq(myShows.id, input.id))
      return { isSaved: false }
    }
  },
)

export const createCheckoutSession = authAction(
  z.object({
    stripeProductId: z.string(),
    planName: z.enum(planTuple),
  }),
  async (input, { userId }) => {
    const account = await getAccount()
    const siteUrl = headers().get("origin")!
    let checkoutSession: Stripe.Checkout.Session | Stripe.BillingPortal.Session
    if (input.planName !== "free" && account.membership === "free")
      checkoutSession = await stripe.checkout.sessions.create({
        mode: "subscription",
        billing_address_collection: "auto",
        customer_email: account.email,
        line_items: [
          {
            price: input.stripeProductId,
            quantity: 1,
          },
        ],
        success_url: `${siteUrl}/subscription/result?session_id={CHECKOUT_SESSION_ID}`,
        cancel_url: `${siteUrl}/subscription`,
        metadata: {
          userId,
          planName: input.planName,
        },
      })
    else
      checkoutSession = await stripe.billingPortal.sessions.create({
        customer: account.stripeCustomerId!,
        return_url: `${siteUrl}/subscription`,
      })
    redirect(checkoutSession.url!)
  },
)

export const getMyShowsInfinite = authAction(
  z.object({
    index: z.number().min(0),
    limit: z.number().min(2).max(50),
  }),
  async (input) => {
    const account = await getAccountWithActiveProfile()
    const shows = await db.query.myShows.findMany({
      where: eq(myShows.profileId, account.activeProfileId),
      limit: input.limit + 1,
      offset: input.index * input.limit,
    })
    const hasNextPage = shows.length > input.limit ? true : false
    if (hasNextPage) shows.pop()
    const filteredShows = await getMyShowsFromTmdb(shows)
    return { shows: filteredShows, hasNextPage }
  },
)


================================================
FILE: src/actions/safe-action-client.ts
================================================
/* eslint-disable @typescript-eslint/require-await */
import { createSafeActionClient } from "next-safe-action"
import { auth } from "@clerk/nextjs"
import { ERR } from "~/lib/utils"

export const action = createSafeActionClient()

export const authAction = createSafeActionClient({
  buildContext: async () => {
    const userId = auth().userId
    if (!userId) throw new Error(ERR.unauthenticated)
    return {
      userId,
    }
  },
})


================================================
FILE: src/app/(auth)/layout.tsx
================================================
export default function AuthLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <div className="grid min-h-screen place-content-center bg-slate-50">
      {children}
    </div>
  )
}


================================================
FILE: src/app/(auth)/sign-in/[[...sign-in]]/page.tsx
================================================
import { SignIn } from "@clerk/nextjs"

export default function Page() {
  return <SignIn />
}


================================================
FILE: src/app/(auth)/sign-up/[[...sign-up]]/page.tsx
================================================
import { SignUp } from "@clerk/nextjs"

export default function Page() {
  return <SignUp />
}


================================================
FILE: src/app/(main)/@modal/(.)show/[id]/modal.tsx
================================================
"use client"
import type { ShowWithVideoAndGenre } from "~/lib/types"
import { useRef, useEffect } from "react"
import { useRouter } from "next/navigation"
import { ModalCard } from "~/components/modal-card"

export function Modal({
  show,
  isSaved,
}: {
  show: ShowWithVideoAndGenre
  isSaved?: boolean
}) {
  const overlay = useRef(null)
  const router = useRouter()

  useEffect(() => {
    const back = (e: KeyboardEvent) => e.key === "Escape" && router.back()
    document.addEventListener("keydown", back)
    return () => document.removeEventListener("keydown", back)
  }, [])

  return (
    <div
      ref={overlay}
      onClick={(e) => e.target === overlay.current && router.back()}
      className="fixed inset-0 bg-black/60"
      id="show-modal"
    >
      <ModalCard
        show={show}
        isSaved={isSaved}
        className="absolute left-1/2 top-1/2 w-full max-w-3xl -translate-x-1/2 -translate-y-1/2"
      />
    </div>
  )
}


================================================
FILE: src/app/(main)/@modal/(.)show/[id]/page.tsx
================================================
import { Modal } from "./modal"
import type { MediaType } from "~/lib/types"
import { getShowVideoAndGenreWithStatus } from "~/lib/server-fetchers"

export default async function ShowModal(props: {
  params: { id: number }
  searchParams: { mediaType: MediaType }
}) {
  const { show, isSaved } = await getShowVideoAndGenreWithStatus(
    props.params.id,
    props.searchParams.mediaType,
  )

  return <Modal show={show} isSaved={isSaved} />
}


================================================
FILE: src/app/(main)/@modal/default.tsx
================================================
export default function Default() {
  return null
}


================================================
FILE: src/app/(main)/@modal/loading.tsx
================================================
import { Skeleton } from "~/components/ui/skeleton"

export default function Loading() {
  return (
    <div className="fixed inset-0 bg-black/60">
      <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" />
    </div>
  )
}


================================================
FILE: src/app/(main)/account/loading.tsx
================================================
import { Skeleton } from "~/components/ui/skeleton"

export default function Loading() {
  return (
    <main className="my-12 flex justify-center">
      <Skeleton className="h-[713px] w-[500px]" />
    </main>
  )
}


================================================
FILE: src/app/(main)/account/page.tsx
================================================
import { CreditCard, ChevronRight } from "lucide-react"
import { Button } from "~/components/ui/button"
import Link from "next/link"
import { getAccountWithProfiles } from "~/lib/server-fetchers"

export default async function AccountPage() {
  const account = await getAccountWithProfiles()
  return (
    <main className="mt-[2.5%] flex justify-center ">
      <div className="space-y-5 md:w-[500px]">
        <section className="space-y-2">
          <h1 className="text-4xl">Account</h1>
          <p className="flex items-center gap-2 text-sm text-neutral-400">
            <CreditCard />
            Member Since: {account.createdAt.toDateString()}
          </p>
        </section>
        <div aria-label="divider" className="h-px w-full bg-white/25" />
        <p className="text-2xl text-neutral-400">MEMBERSHIP & BILLING</p>
        <p className="flex cursor-pointer justify-between">
          {account.email}
          <ChevronRight />
        </p>
        <div aria-label="divider" className="h-px w-full bg-white/25" />
        <p className="flex cursor-pointer justify-between">
          Update Account
          <ChevronRight />
        </p>
        <div aria-label="divider" className="h-px w-full bg-white/25" />
        <Button className="w-full" asChild>
          <Link href="/subscription">Manage Subscription</Link>
        </Button>
        <div aria-label="divider" className="h-px w-full bg-white/25" />
        <p className="text-2xl text-neutral-400">Plan Details</p>
        <p className="flex gap-1.5">
          {`${account.membership
            .charAt(0)
            .toUpperCase()}${account.membership.substring(1)}`}
          <span className="rounded-sm px-1 text-neutral-100 ring-2 ring-slate-100">
            4K+HDR
          </span>
        </p>
        <div aria-label="divider" className="h-px w-full bg-white/25" />
        <p className="flex cursor-pointer justify-between">
          Change plan
          <ChevronRight />
        </p>
        <div aria-label="divider" className="h-px w-full bg-white/25" />
        <p className="text-2xl text-neutral-400">Profiles</p>
        <div className="flex gap-4 md:gap-8">
          {account.profiles.map((profile) => (
            <div key={profile.id} className="space-y-1.5">
              {/* eslint-disable-next-line @next/next/no-img-element */}
              <img
                src={profile.profileImgPath}
                alt="profile-img"
                className="w-14 rounded-lg md:w-24"
              />
              <h3 className="text-center text-sm md:text-base">
                {profile.name}
              </h3>
            </div>
          ))}
        </div>
        <div aria-label="divider" className="h-px w-full bg-white/25" />
      </div>
    </main>
  )
}


================================================
FILE: src/app/(main)/default.tsx
================================================
export default function Default() {
  return null
}


================================================
FILE: src/app/(main)/layout.tsx
================================================
import Image from "next/image"
import Link from "next/link"
import { currentUser, SignedOut, auth, SignOutButton } from "@clerk/nextjs"
import { Suspense } from "react"
import { Button } from "~/components/ui/button"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "~/components/ui/dropdown-menu"
import { Skeleton } from "~/components/ui/skeleton"
import { db } from "~/db/client"
import { accounts, profiles } from "~/db/schema"
import { eq } from "drizzle-orm"
import { ERR } from "~/lib/utils"
import {
  Search,
  Bell,
  Facebook,
  Instagram,
  Twitter,
  Youtube,
  Home,
  Clapperboard,
  Film,
  TrendingUp,
  List,
  Pencil,
  ArrowLeftRight,
  User,
  BadgeCheck,
} from "lucide-react"
import { LinkButton } from "~/components/link-button"
import { getAccountWithActiveProfile } from "~/lib/server-fetchers"
import { OverlayScrollbar } from "~/components/overlay-scrollbar"

export default function ShowsLayout({
  children,
  modal,
}: {
  children: React.ReactNode
  modal: React.ReactNode
}) {
  return (
    <div className="container flex min-h-screen flex-col px-4 md:px-8">
      <Header />
      {children}
      {modal}
      <Footer />
      <OverlayScrollbar />
    </div>
  )
}

const NAVINFO = [
  { name: "Home", href: "/", icon: <Home className="w-5" /> },
  {
    name: "TV Shows",
    href: "/tv-shows",
    icon: <Clapperboard className="w-5" />,
  },
  { name: "Movies", href: "/movies", icon: <Film className="w-5" /> },
  {
    name: "New & Popular",
    href: "/new-and-popular",
    icon: <TrendingUp className="w-5" />,
  },
  { name: "My List", href: "/my-list", icon: <List className="w-5" /> },
]

function Header() {
  return (
    <header className="flex h-16 justify-between">
      <div className="flex items-center gap-12">
        <Link href="/" className="hidden md:block">
          <Image
            src="/netflix-logo.svg"
            alt="netflix-logo"
            width={300}
            height={81}
            priority
            className="h-auto w-28 transition-opacity hover:opacity-80 active:opacity-100"
          />
        </Link>
        <MainMenu />
        <nav className="hidden gap-6 text-sm md:flex">
          {NAVINFO.map((el) =>
            el.name === "My List" ? (
              <LinkButton href={el.href} key={el.name}>
                {el.name}
              </LinkButton>
            ) : (
              <Link href={el.href} key={el.name}>
                {el.name}
              </Link>
            ),
          )}
        </nav>
      </div>
      <div className="flex items-center gap-6">
        <Link href="/search?keyword=" aria-label="search">
          <Search />
        </Link>
        <Bell />
        <Suspense fallback={<Skeleton className="h-8 w-8" />}>
          <CustomeUserButton />
        </Suspense>
        <SignedOut>
          <Button
            asChild
            className="bg-red-600 font-semibold text-white hover:bg-red-700 active:bg-red-800"
          >
            <Link href="/sign-in">Sign In</Link>
          </Button>
        </SignedOut>
      </div>
    </header>
  )
}

async function CustomeUserButton() {
  const { userId } = auth()
  if (!userId) return
  const existingAccount = await db.query.accounts.findFirst({
    where: eq(accounts.id, userId),
    with: { activeProfile: true },
  })
  const account = existingAccount ?? (await createAccountAndProfile())
  return (
    <DropdownMenu modal={false}>
      <DropdownMenuTrigger>
        {/* eslint-disable-next-line @next/next/no-img-element */}
        <img
          src={account.activeProfile.profileImgPath}
          alt="user-image"
          height="32"
          width="32"
          className="rounded-sm"
        />
      </DropdownMenuTrigger>
      <DropdownMenuContent>
        <DropdownMenuLabel>{account.activeProfile.name}</DropdownMenuLabel>
        <DropdownMenuSeparator />
        <Link href="/manage-profile">
          <DropdownMenuItem className="gap-1.5">
            <Pencil className="w-5" />
            Manage Profile
          </DropdownMenuItem>
        </Link>
        <Link href="/switch-profile">
          <DropdownMenuItem className="gap-1.5">
            <ArrowLeftRight className="w-5" />
            Switch Profile
          </DropdownMenuItem>
        </Link>
        <Link href="/account">
          <DropdownMenuItem className="gap-1.5">
            <User className="w-5" />
            Account
          </DropdownMenuItem>
        </Link>
        <Link href="/subscription">
          <DropdownMenuItem className="gap-1.5">
            <BadgeCheck className="w-5" />
            Subscription
          </DropdownMenuItem>
        </Link>
        <DropdownMenuItem>
          <SignOutButton>
            <Button className="w-full font-semibold">Sign Out</Button>
          </SignOutButton>
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

function MainMenu() {
  return (
    <DropdownMenu modal={false}>
      <DropdownMenuTrigger className="flex items-center gap-1.5 md:hidden">
        <svg viewBox="0 0 24 24" className="h-5 w-5 text-red-600">
          <path
            fill="currentColor"
            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"
          ></path>
        </svg>
        <h2 className="font-semibold">Menu</h2>
      </DropdownMenuTrigger>
      <DropdownMenuContent>
        <DropdownMenuLabel className="flex gap-1.5">
          <svg viewBox="0 0 24 24" className="w-5 text-red-600">
            <path
              fill="currentColor"
              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"
            ></path>
          </svg>
          Netflix
        </DropdownMenuLabel>
        <DropdownMenuSeparator />
        {NAVINFO.map((el) => (
          <Link href={el.href} key={el.name}>
            <DropdownMenuItem className="gap-1.5">
              {el.icon}
              {el.name}
            </DropdownMenuItem>
          </Link>
        ))}
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

function Footer() {
  return (
    <footer className="mt-auto pb-3 pt-12 text-sm">
      <i className="flex gap-3 py-3">
        <Link href="/" aria-label="facebook">
          <Facebook className="hover:text-red-500" />
        </Link>
        <Link href="/" className="hover:text-red-500" aria-label="instagram">
          <Instagram />
        </Link>
        <Link href="/" className="hover:text-red-500" aria-label="twitter">
          <Twitter />
        </Link>
        <Link href="/" className="hover:text-red-500" aria-label="youtube">
          <Youtube />
        </Link>
      </i>
      <div className="grid grid-cols-2 justify-between gap-y-3 py-3 text-xs text-white/50 md:flex md:text-sm">
        <div className="flex flex-col gap-3">
          <Link href="/">Audio Description</Link>
          <Link href="/">Investor Relations</Link>
          <Link href="/">Legal Notices</Link>
        </div>
        <div className="flex flex-col gap-3">
          <Link href="/">Help Center</Link>
          <Link href="/">Jobs</Link>
          <Link href="/">Cookie Preferences</Link>
        </div>
        <div className="flex flex-col gap-3">
          <Link href="/">Gift Cards</Link>
          <Link href="/">Terms of Use</Link>
          <Link href="/">Corporate Information</Link>
        </div>
        <div className="flex flex-col gap-3">
          <Link href="/">Media Center</Link>
          <Link href="/">Privacy</Link>
          <Link href="/">Contact Us</Link>
        </div>
      </div>
      <div className="text-center font-semibold text-neutral-300">
        Built by Apestein. The source code is available on&nbsp;
        <a
          href="https://github.com/Apestein/nextflix"
          target="_blank"
          rel="noreferrer"
          className="font-medium underline underline-offset-4"
        >
          Github
        </a>
      </div>
    </footer>
  )
}

async function createAccountAndProfile() {
  const user = await currentUser()
  if (!user) throw new Error(ERR.unauthenticated)
  await db
    .insert(accounts)
    .values({
      id: user.id,
      email: user.emailAddresses[0]!.emailAddress,
      activeProfileId: user.id + "-1",
    })
    .onConflictDoNothing()
  await db
    .insert(profiles)
    .values({
      id: user.id + "-1",
      accountId: user.id,
      profileImgPath: `https://api.dicebear.com/6.x/bottts-neutral/svg?seed=${
        user.username ?? user.firstName ?? user.emailAddresses[0]!.emailAddress
      }`,
      name:
        user.username ?? user.firstName ?? user.emailAddresses[0]!.emailAddress,
    })
    .onConflictDoNothing()
  return getAccountWithActiveProfile()
}


================================================
FILE: src/app/(main)/loading.tsx
================================================
import { Skeleton } from "~/components/ui/skeleton"

export default function Loading() {
  return (
    <main>
      <div className="space-y-10">
        <Skeleton className="my-16 h-[250px] w-full max-w-lg md:h-[384px]" />
        <Skeleton className="h-[138px] w-full md:h-[189px]" />
        <Skeleton className="h-[138px] w-full md:h-[189px]" />
        <Skeleton className="h-[138px] w-full md:h-[189px]" />
      </div>
    </main>
  )
}


================================================
FILE: src/app/(main)/movies/page.tsx
================================================
import { ShowsCarousel } from "~/components/show-carousel"
import { getShows } from "~/lib/client-fetchers"
import { ShowBg } from "../../../components/show-bg"
import { ShowHero } from "../../../components/show-hero"
import { pickRandomShow } from "~/lib/utils"

export default async function Movies() {
  const allShows = await getShows("movie")
  const randomShow = pickRandomShow(allShows.trending)

  return (
    <>
      <ShowBg show={randomShow} />
      <main>
        <ShowHero show={randomShow} />
        <div className="space-y-10">
          <ShowsCarousel title="Trending" shows={allShows.trending} />
          <ShowsCarousel title="Top Rated" shows={allShows.topRated} />
          <ShowsCarousel
            title="Action Thriller"
            shows={allShows.actionThriller}
          />
          <ShowsCarousel title="Comedy" shows={allShows.comedy} />
          <ShowsCarousel title="Horror" shows={allShows.horror} />
          <ShowsCarousel title="Romance" shows={allShows.romance} />
          <ShowsCarousel title="Documentary" shows={allShows.documentary} />
        </div>
      </main>
    </>
  )
}


================================================
FILE: src/app/(main)/my-list/infinite-scroller.tsx
================================================
"use client"
import { useEffect, useRef, useState } from "react"
import type { Show } from "~/lib/types"
import { getMyShowsInfinite } from "~/actions"
import { getShows } from "~/lib/client-fetchers"
import { ERR } from "~/lib/utils"
import { Button } from "~/components/ui/button"
import Link from "next/link"

export function ShowScroller({
  initialShows,
  initialHasNextPage,
  limit,
}: {
  initialShows: Show[]
  initialHasNextPage: boolean
  limit: number
}) {
  const [myShows, setMyShows] = useState(initialShows)
  const [simulatedShows, setSimulatedShows] = useState<Show[]>()
  const getShowsReturnRef = useRef<Awaited<ReturnType<typeof getShows>>>()
  const hasNextPageRef = useRef(initialHasNextPage)
  const indexRef = useRef(0)
  const observerTarget = useRef(null)

  const shows = simulatedShows ?? myShows

  async function fetchNextPage() {
    indexRef.current += 1
    const { data } = await getMyShowsInfinite({
      index: indexRef.current,
      limit,
    })
    if (!data) throw new Error(ERR.db)
    setMyShows((prev) => [...prev, ...data.shows])
    hasNextPageRef.current = data.hasNextPage
  }

  async function getSimulatedShows() {
    if (getShowsReturnRef.current) {
      window.scrollTo(0, 0)
      getShowsReturnRef.current = undefined
      hasNextPageRef.current = initialHasNextPage
      setSimulatedShows(undefined)
      return
    }
    window.scrollTo(0, 0)
    const data = await getShows("movie")
    getShowsReturnRef.current = data
    hasNextPageRef.current = true
    setSimulatedShows([
      ...new Map(
        [...data.trending, ...data.topRated].map((item) => [item.id, item]),
      ).values(),
    ])
  }

  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        if (!hasNextPageRef.current) return
        if (entries[0]?.isIntersecting) {
          if (getShowsReturnRef.current) {
            setTimeout(
              () =>
                setSimulatedShows((prev) => [
                  ...new Map(
                    [
                      ...prev!,
                      ...getShowsReturnRef.current!.actionThriller,
                      ...getShowsReturnRef.current!.comedy,
                    ].map((item) => [item.id, item]),
                  ).values(),
                ]),
              1000,
            )
            hasNextPageRef.current = false
          } else void fetchNextPage()
        }
      },
      { threshold: 1 },
    )

    if (observerTarget.current) {
      observer.observe(observerTarget.current)
    }

    return () => observer.disconnect()
  }, [])

  return (
    <main className="space-y-1.5 [overflow-anchor:none]">
      <ul className="grid grid-cols-[repeat(auto-fill,_minmax(160px,_1fr))] gap-5 md:grid-cols-[repeat(auto-fill,_minmax(240px,_1fr))]">
        {shows.map((show) => (
          <Link
            href={`/show/${show.id}?mediaType=${show.title ? "movie" : "tv"}`}
            scroll={false}
            key={show.id}
          >
            {/* eslint-disable-next-line @next/next/no-img-element */}
            <img
              src={`https://image.tmdb.org/t/p/w300${
                show.backdrop_path ?? show.poster_path
              }`}
              alt="show-backdrop"
              width={300}
              height={169}
              className="aspect-video max-w-full cursor-pointer object-cover transition-transform hover:scale-110"
            />
          </Link>
        ))}
      </ul>
      <div ref={observerTarget}></div>
      <section className="flex flex-col items-center gap-4">
        {hasNextPageRef.current ? (
          <Button
            variant="outline"
            className="w-full animate-pulse"
            // eslint-disable-next-line @typescript-eslint/no-misused-promises
            onClick={fetchNextPage}
          >
            Loading...
          </Button>
        ) : (
          <Button variant="outline" className="w-full cursor-auto">
            You have reached the end of your saved shows
          </Button>
        )}
        {/* eslint-disable-next-line @typescript-eslint/no-misused-promises */}
        <Button onClick={getSimulatedShows}>
          {simulatedShows ? "Turn off simulation" : "Simulate many saved shows"}
        </Button>
      </section>
    </main>
  )
}


================================================
FILE: src/app/(main)/my-list/loading.tsx
================================================
import { Skeleton } from "~/components/ui/skeleton"

export default function Loading() {
  return (
    <main className="pt-8">
      <Skeleton className="h-96 w-full" />
    </main>
  )
}


================================================
FILE: src/app/(main)/my-list/page.tsx
================================================
import { ShowScroller } from "./infinite-scroller"
import { getMyShows } from "~/lib/server-fetchers"

export default async function MyShowPage() {
  const LIMIT = 30
  const data = await getMyShows(LIMIT)
  return (
    <main className="pt-8">
      {!data.shows.length && (
        <div className="mb-4 space-y-3">
          <p className="text-3xl font-semibold">Your list is empty</p>
          <p className="text-white/60">
            Add shows and movies to your list to watch them later
          </p>
        </div>
      )}
      <ShowScroller
        initialShows={data.shows}
        initialHasNextPage={data.hasNextPage}
        limit={LIMIT}
      />
    </main>
  )
}


================================================
FILE: src/app/(main)/new-and-popular/page.tsx
================================================
import type { Show } from "~/lib/types"
import { ShowsCarousel } from "~/components/show-carousel"
import { ERR } from "~/lib/utils"
import { env } from "~/env.mjs"
import { ShowBg } from "../../../components/show-bg"
import { ShowHero } from "../../../components/show-hero"
import { pickRandomShow } from "~/lib/utils"

export default async function NewAndPopular() {
  const newAndPopularShows = await getNewAndPopularShows()
  const randomShow = pickRandomShow(newAndPopularShows.trendingMovies)

  return (
    <>
      <ShowBg show={randomShow} />
      <main>
        <ShowHero show={randomShow} />
        <div className="space-y-10">
          <ShowsCarousel
            title="Popular Movies"
            shows={newAndPopularShows.popularMovies}
          />
          <ShowsCarousel
            title="Popular TV Shows"
            shows={newAndPopularShows.popularTvs}
          />
          <ShowsCarousel
            title="Trending Movies"
            shows={newAndPopularShows.trendingMovies}
          />
          <ShowsCarousel
            title="Trending TV Shows"
            shows={newAndPopularShows.trendingTvs}
          />
        </div>
      </main>
    </>
  )
}

async function getNewAndPopularShows() {
  const [popularTvRes, popularMovieRes, trendingTvRes, trendingMovieRes] =
    await Promise.all([
      fetch(
        `https://api.themoviedb.org/3/tv/popular?api_key=${env.NEXT_PUBLIC_TMDB_API}`,
      ),
      fetch(
        `https://api.themoviedb.org/3/movie/popular?api_key=${env.NEXT_PUBLIC_TMDB_API}`,
      ),
      fetch(
        `https://api.themoviedb.org/3/trending/tv/day?api_key=${env.NEXT_PUBLIC_TMDB_API}`,
      ),
      fetch(
        `https://api.themoviedb.org/3/trending/movie/day?api_key=${env.NEXT_PUBLIC_TMDB_API}`,
      ),
    ])

  if (
    !popularTvRes.ok ||
    !popularMovieRes.ok ||
    !trendingTvRes.ok ||
    !trendingMovieRes.ok
  ) {
    throw new Error(ERR.fetch)
  }

  const [popularTvs, popularMovies, trendingTvs, trendingMovies] =
    await Promise.all<{ results: Show[] }>([
      popularTvRes.json(),
      popularMovieRes.json(),
      trendingTvRes.json(),
      trendingMovieRes.json(),
    ])

  if (!popularTvs || !popularMovies || !trendingTvs || !trendingMovies)
    throw new Error(ERR.fetch)

  return {
    popularTvs: popularTvs.results,
    popularMovies: popularMovies.results,
    trendingTvs: trendingTvs.results,
    trendingMovies: trendingMovies.results,
  }
}


================================================
FILE: src/app/(main)/page.tsx
================================================
import { getShows } from "~/lib/client-fetchers"
import { ShowHero } from "~/components/show-hero"
import { ShowBg } from "~/components/show-bg"
import { pickRandomShow } from "~/lib/utils"
import { ShowsCarousel } from "~/components/show-carousel"

export default async function Home() {
  const allShows = await getShows("movie")
  const randomShow = pickRandomShow(allShows.trending)

  return (
    <>
      <ShowBg show={randomShow} />
      <main>
        <ShowHero show={randomShow} />
        <div className="space-y-10">
          <ShowsCarousel title="Trending" shows={allShows.trending} />
          <ShowsCarousel title="Top Rated" shows={allShows.topRated} />
          <ShowsCarousel
            title="Action Thriller"
            shows={allShows.actionThriller}
          />
          <ShowsCarousel title="Comedy" shows={allShows.comedy} />
          <ShowsCarousel title="Horror" shows={allShows.horror} />
          <ShowsCarousel title="Romance" shows={allShows.romance} />
          <ShowsCarousel title="Documentary" shows={allShows.documentary} />
        </div>
      </main>
    </>
  )
}


================================================
FILE: src/app/(main)/search/loading.tsx
================================================
import { Skeleton } from "~/components/ui/skeleton"

export default function Loading() {
  return (
    <main className="mt-8">
      <Skeleton className="h-96 w-full" />
    </main>
  )
}


================================================
FILE: src/app/(main)/search/page.tsx
================================================
import { ERR } from "~/lib/utils"
import { env } from "~/env.mjs"
import type { Show } from "~/lib/types"
import { SearchInput } from "./search-input"
import Link from "next/link"

export default async function SearchPage({
  searchParams,
}: {
  searchParams: { keyword: string }
}) {
  if (!searchParams.keyword)
    return (
      <main>
        <SearchInput initialQuery="" className="my-8" />
      </main>
    )
  const shows = await searchShows(searchParams.keyword)
  return (
    <main>
      <SearchInput initialQuery={searchParams.keyword} className="my-8" />
      <div className="grid grid-cols-[repeat(auto-fill,_minmax(160px,_1fr))] gap-4 md:grid-cols-[repeat(auto-fill,_minmax(240px,_1fr))]">
        {shows.map((show) =>
          show.backdrop_path || show.poster_path ? (
            <Link
              href={`/show/${show.id}?mediaType=${show.title ? "movie" : "tv"}`}
              scroll={false}
              key={show.id}
            >
              {/* eslint-disable-next-line @next/next/no-img-element */}
              <img
                src={`https://image.tmdb.org/t/p/w300${
                  show.backdrop_path ?? show.poster_path
                }`}
                alt="show-backdrop"
                width={300}
                height={169}
                className="aspect-video max-w-full cursor-pointer object-cover transition-transform hover:scale-110"
              />
            </Link>
          ) : null,
        )}
      </div>
    </main>
  )
}

async function searchShows(query: string) {
  const res = await fetch(
    `https://api.themoviedb.org/3/search/multi?api_key=${env.NEXT_PUBLIC_TMDB_API}&query=${query}`,
  )
  if (!res.ok) throw new Error(ERR.fetch)
  const shows = (await res.json()) as { results: Show[] }
  const popularShows = shows.results.sort((a, b) => b.popularity - a.popularity)
  return popularShows
}


================================================
FILE: src/app/(main)/search/search-input.tsx
================================================
"use client"
import { Input } from "~/components/ui/input"
import { useEffect, useState } from "react"
import { useDebouncedCallback } from "use-debounce"
import { useRouter } from "next/navigation"

interface PageProps extends React.HTMLAttributes<HTMLElement> {
  initialQuery: string
}
export function SearchInput({ initialQuery, ...props }: PageProps) {
  const [query, setQuery] = useState("")
  const debounced = useDebouncedCallback((value: string) => {
    setQuery(value)
  }, 500)
  const router = useRouter()

  useEffect(() => {
    if (query) router.replace(`/search?keyword=${query}`)
  }, [query])
  return (
    <Input
      placeholder="search keyword"
      defaultValue={initialQuery}
      onChange={(e) => debounced(e.target.value)}
      autoFocus
      {...props}
    />
  )
}


================================================
FILE: src/app/(main)/show/[id]/page.tsx
================================================
import { ModalCard } from "~/components/modal-card"
import type { MediaType } from "~/lib/types"
import { getShowVideoAndGenreWithStatus } from "~/lib/server-fetchers"

export default async function ShowPage(props: {
  params: { id: number }
  searchParams: { mediaType: MediaType }
}) {
  const { show, isSaved } = await getShowVideoAndGenreWithStatus(
    props.params.id,
    props.searchParams.mediaType,
  )
  return (
    <main className="mt-4">
      <ModalCard
        show={show}
        isSaved={isSaved}
        className="mx-auto w-full max-w-3xl"
      />
    </main>
  )
}


================================================
FILE: src/app/(main)/subscription/loading.tsx
================================================
import { Skeleton } from "~/components/ui/skeleton"

export default function Loading() {
  return (
    <main>
      <Skeleton className="h-[50vh] w-full" />
    </main>
  )
}


================================================
FILE: src/app/(main)/subscription/page.tsx
================================================
import { Check } from "lucide-react"
import { PlanSelector } from "./plan-selector"
import { getAccount } from "~/lib/server-fetchers"

export default async function SubscriptionPage() {
  const account = await getAccount()
  return (
    <main className="mt-8 space-y-8 md:px-24">
      <h1 className="text-3xl font-bold sm:text-4xl">
        Choose the plan that&apos;s right for you
      </h1>
      <div className="space-y-3 text-zinc-400">
        <div className="flex gap-1.5">
          <Check stroke="red" />
          <p>Watch on your phone, tablet, laptop, and TV</p>
        </div>
        <div className="flex gap-1.5">
          <Check stroke="red" />
          <p>Unlimited movies and TV shows</p>
        </div>
        <div className="flex gap-1.5">
          <Check stroke="red" />
          <p>Change or cancel your plan anytime</p>
        </div>
      </div>
      <PlanSelector activeSubscription={account.membership} />
      <div className="space-y-3 text-sm text-zinc-300">
        <p>
          HD (720p), Full HD (1080p), Ultra HD (4K) and HDR availability subject
          to your internet service and device capabilities. Not all content is
          available in all resolutions. See our{" "}
          <span className="cursor-pointer text-blue-500">Terms of Use</span> for
          more details.
        </p>
        <p>
          Only people who live with you may use your account. Watch on 4
          different devices at the same time with Premium, 2 with Standard, and
          1 with Basic and Mobile.
        </p>
      </div>
    </main>
  )
}


================================================
FILE: src/app/(main)/subscription/plan-selector.tsx
================================================
"use client"
import { cn } from "~/lib/utils"
import { useState } from "react"
import { Button } from "~/components/ui/button"
import type { SubscriptionPlan, PlanName } from "~/lib/types"
import { PLANS } from "~/lib/configs"
import { createCheckoutSession } from "~/actions"

export function PlanSelector({
  activeSubscription,
}: {
  activeSubscription: PlanName
}) {
  const [selectedPlan, setSelectedPlan] = useState<SubscriptionPlan>(
    Plans[activeSubscription],
  )

  function submit() {
    void createCheckoutSession({
      stripeProductId: selectedPlan.id,
      planName: selectedPlan.name,
    })
  }

  return (
    <>
      <div className="flex justify-end gap-1.5 md:gap-8">
        {PLANS.map((plan) => (
          <div
            key={plan.id}
            className={cn(
              "grid aspect-square w-20 shrink-0 cursor-pointer place-content-center rounded-lg font-semibold md:w-24",
              selectedPlan.name === plan.name
                ? "bg-red-600"
                : "bg-red-900 hover:bg-red-700",
            )}
            onClick={() => setSelectedPlan(plan)}
          >
            {`${plan.name.charAt(0).toUpperCase()}${plan.name.substring(1)}`}
          </div>
        ))}
      </div>
      <div className="flex justify-end">
        <Button
          className="w-56 bg-green-600 font-semibold text-white hover:bg-green-700"
          onClick={submit}
          disabled={
            selectedPlan.name === "free" && activeSubscription === "free"
              ? true
              : false
          }
        >
          {activeSubscription !== "free" ? "Edit" : "Subscribe"}
        </Button>
      </div>
    </>
  )
}

const Plans = {
  free: PLANS[0],
  basic: PLANS[1],
  standard: PLANS[2],
  premium: PLANS[3],
}


================================================
FILE: src/app/(main)/subscription/result/page.tsx
================================================
// import type { Stripe } from "stripe"
import { stripe } from "~/lib/stripe"
import { ScrollArea } from "~/components/ui/scroll-area"

export default async function ResultPage({
  searchParams,
}: {
  searchParams: { session_id: string }
}): Promise<JSX.Element> {
  if (!searchParams.session_id)
    throw new Error("Please provide a valid session_id (`cs_test_...`)")

  const checkoutSession = await stripe.checkout.sessions.retrieve(
    searchParams.session_id,
    {
      expand: ["line_items", "payment_intent"],
    },
  )

  const checkoutStatus = checkoutSession.payment_status
  const currencyFormatter = new Intl.NumberFormat("en-US", {
    style: "currency",
    currency: "USD",
  })
  const amountPaid = currencyFormatter.format(
    checkoutSession.amount_subtotal! / 100,
  )
  const createdAt = new Date(checkoutSession.created)
  return (
    <main className="mt-8 space-y-4">
      <h2 className="text-xl font-semibold">
        Checkout Status: {checkoutStatus}
      </h2>
      <h3>Checkout Session Info:</h3>
      <ScrollArea className="rounded-md border p-4">
        {/* <PrintObject content={checkoutSession} /> */}
        <p>{`Email: ${checkoutSession.customer_email}`}</p>
        <p>{`Amount Total: ${amountPaid}`}</p>
        <p>{`Time: ${createdAt.toUTCString()}`}</p>
      </ScrollArea>
    </main>
  )
}

// function PrintObject({
//   content,
// }: {
//   content: Stripe.Response<Stripe.Checkout.Session>
// }): JSX.Element {
//   const formattedContent: string = JSON.stringify(content, null, 2)
//   return <pre>{formattedContent}</pre>
// }


================================================
FILE: src/app/(main)/tv-shows/page.tsx
================================================
import { ShowsCarousel } from "~/components/show-carousel"
import { getShows } from "~/lib/client-fetchers"
import { ShowBg } from "../../../components/show-bg"
import { ShowHero } from "../../../components/show-hero"
import { pickRandomShow } from "~/lib/utils"

export default async function TvShows() {
  const allShows = await getShows("tv")
  const randomShow = pickRandomShow(allShows.topRated)

  return (
    <>
      <ShowBg show={randomShow} />
      <main>
        <ShowHero show={randomShow} />
        <div className="space-y-10">
          <ShowsCarousel title="Trending" shows={allShows.trending} />
          <ShowsCarousel title="Top Rated" shows={allShows.topRated} />
          <ShowsCarousel
            title="Action Thriller"
            shows={allShows.actionThriller}
          />
          <ShowsCarousel title="Comedy" shows={allShows.comedy} />
          <ShowsCarousel title="Horror" shows={allShows.horror} />
          <ShowsCarousel title="Romance" shows={allShows.romance} />
          <ShowsCarousel title="Documentary" shows={allShows.documentary} />
        </div>
      </main>
    </>
  )
}


================================================
FILE: src/app/(profile)/loading.tsx
================================================
import { Skeleton } from "~/components/ui/skeleton"

export default function Loading() {
  return (
    <main className="grid min-h-screen place-content-center">
      <Skeleton className="h-[50vh] w-[98vw] md:w-[500px]" />
    </main>
  )
}


================================================
FILE: src/app/(profile)/manage-profile/[...slug]/page.tsx
================================================
"use client"
import { Button } from "~/components/ui/button"
import { useState } from "react"
import { useDebouncedCallback } from "use-debounce"
import { useRouter } from "next/navigation"
import { useToast } from "~/components/ui/use-toast"
import { Input } from "~/components/ui/input"
import { ArrowLeft } from "lucide-react"
import Link from "next/link"
import { deleteProfile, updateProfile } from "~/actions"

export default function ProfilePage({
  params,
  searchParams,
}: {
  params: { slug: string[] }
  searchParams: { profileId: string }
}) {
  const [name, setName] = useState(params.slug[0]!)
  const debounced = useDebouncedCallback((value: string) => {
    setName(value)
  }, 500)
  const router = useRouter()
  const { toast } = useToast()

  async function doDelete() {
    const { data, validationError } = await deleteProfile({
      profileId: searchParams.profileId,
    })
    toast({
      description: data?.message ?? validationError?.profileId,
    })
    if (data) router.replace("/manage-profile")
  }

  async function doUpdate() {
    const { data, validationError } = await updateProfile({
      profileId: searchParams.profileId,
      name,
    })
    toast({
      description: data?.message ?? JSON.stringify(validationError, null, 4),
    })
    if (data) router.replace("/manage-profile")
  }

  return (
    <>
      <Button variant="outline" asChild className="absolute ml-6 mt-6">
        <Link href="/manage-profile">
          <ArrowLeft />
        </Link>
      </Button>
      <main className="grid min-h-screen place-content-center place-items-center gap-y-8">
        <div className="w-full space-y-3 border-b border-white/25 pb-3 text-center">
          <h1 className="text-3xl md:text-5xl">Update Profile</h1>
          <p className="text-white/60">
            Update a profile with a new name and avatar.
          </p>
        </div>
        {/* eslint-disable-next-line @next/next/no-img-element */}
        <img
          src={`https://api.dicebear.com/6.x/bottts-neutral/svg?seed=${name}`}
          alt="profile-image"
          width="135"
          height="135"
        />
        <Input
          defaultValue={name}
          onChange={(e) => debounced(e.target.value)}
        />
        <section className="space-x-8">
          <Button
            className="bg-green-600 font-semibold text-white hover:bg-green-700 active:bg-green-800"
            // eslint-disable-next-line @typescript-eslint/no-misused-promises
            onClick={doUpdate}
          >
            Update
          </Button>
          <Button
            className="bg-red-600 font-semibold text-white hover:bg-red-700 active:bg-red-800"
            // eslint-disable-next-line @typescript-eslint/no-misused-promises
            onClick={doDelete}
          >
            Delete
          </Button>
        </section>
      </main>
    </>
  )
}


================================================
FILE: src/app/(profile)/manage-profile/add/page.tsx
================================================
"use client"
import { Button } from "~/components/ui/button"
import { Input } from "~/components/ui/input"
import { useState } from "react"
import { useDebouncedCallback } from "use-debounce"
import { useRouter } from "next/navigation"
import Link from "next/link"
import { ArrowLeft } from "lucide-react"
import { useToast } from "~/components/ui/use-toast"
import { createProfile } from "~/actions"

export default function AddProfilePage() {
  const [name, setName] = useState("")
  const debounced = useDebouncedCallback((value: string) => {
    setName(value)
  }, 500)
  const router = useRouter()
  const { toast } = useToast()

  async function doAdd() {
    const { data, validationError } = await createProfile({ name })
    toast({
      description:
        data?.message ?? validationError?.name ?? "Name must be unique",
    })
    if (data) router.replace("/manage-profile")
  }

  return (
    <>
      <Button variant="outline" asChild className="absolute ml-6 mt-6">
        <Link href="/manage-profile">
          <ArrowLeft />
        </Link>
      </Button>
      <main className="grid min-h-screen place-content-center place-items-center gap-y-8">
        <div className="w-full space-y-3 border-b border-white/25 pb-3 text-center">
          <h1 className="text-3xl md:text-5xl">Add Profile</h1>
          <p className="text-white/60">
            Add a profile for another person watching Netflix.
          </p>
        </div>
        {/* eslint-disable-next-line @next/next/no-img-element */}
        <img
          src={`https://api.dicebear.com/6.x/bottts-neutral/svg?seed=${name}`}
          alt="profile-image"
          width="135"
          height="135"
        />
        <Input placeholder="name" onChange={(e) => debounced(e.target.value)} />
        {/* eslint-disable-next-line @typescript-eslint/no-misused-promises */}
        <Button onClick={doAdd}>Save</Button>
      </main>
    </>
  )
}


================================================
FILE: src/app/(profile)/manage-profile/page.tsx
================================================
import { Button } from "~/components/ui/button"
import { PlusCircle, ArrowLeft, Pencil } from "lucide-react"
import Link from "next/link"
import { getAccountWithProfiles } from "~/lib/server-fetchers"

export default async function ManageProfilePage() {
  const account = await getAccountWithProfiles()
  return (
    <>
      <Button variant="outline" asChild className="absolute ml-6 mt-6">
        <Link href="/">
          <ArrowLeft />
        </Link>
      </Button>
      <main className="grid min-h-screen place-content-center">
        <section className="space-y-8">
          <h1 className="text-center text-3xl md:text-5xl">Manage Profiles</h1>
          <ul className="grid grid-cols-2 gap-4 md:flex">
            {account.profiles.map((profile) => (
              <div key={profile.id} className="space-y-1.5">
                <Link
                  href={`/manage-profile/${profile.name}?profileId=${profile.id}`}
                  className="relative"
                >
                  {/* eslint-disable-next-line @next/next/no-img-element */}
                  <img
                    src={profile.profileImgPath}
                    alt="profile-image"
                    width="96"
                    height="96"
                  />
                  <div className="absolute inset-0 grid h-full w-24 place-items-center bg-black/25 hover:bg-transparent">
                    <Pencil />
                  </div>
                </Link>
                <h3 className="text-center">{profile.name}</h3>
              </div>
            ))}
            {account.profiles.length !== 4 && (
              <Link href="/manage-profile/add">
                <PlusCircle
                  className="h-24 w-24 bg-neutral-800 p-3 outline-1 hover:outline"
                  strokeWidth={1}
                />
              </Link>
            )}
          </ul>
        </section>
      </main>
    </>
  )
}


================================================
FILE: src/app/(profile)/switch-profile/page.tsx
================================================
import { Button } from "~/components/ui/button"
import { ArrowLeft } from "lucide-react"
import Link from "next/link"
import { ProfileSwitcher } from "./profile-switcher"
import { getAccountWithProfiles } from "~/lib/server-fetchers"

export default async function SwitchProfilePage() {
  const account = await getAccountWithProfiles()
  return (
    <>
      <Button variant="outline" asChild className="absolute ml-6 mt-6">
        <Link href="/">
          <ArrowLeft />
        </Link>
      </Button>
      <main className="grid min-h-screen place-content-center">
        <section className="space-y-8">
          <h1 className="text-center text-3xl md:text-5xl">
            Who&apos;s Watching
          </h1>
          <ul className="grid grid-cols-2 gap-4 md:flex">
            {account.profiles.map((profile) => (
              <ProfileSwitcher key={profile.id} profile={profile} />
            ))}
          </ul>
        </section>
      </main>
    </>
  )
}


================================================
FILE: src/app/(profile)/switch-profile/profile-switcher.tsx
================================================
"use client"
import type { Profile } from "~/lib/types"
import { useRouter } from "next/navigation"
import { useToast } from "~/components/ui/use-toast"
import { switchProfile } from "~/actions"

export function ProfileSwitcher({ profile }: { profile: Profile }) {
  const router = useRouter()
  const { toast } = useToast()

  async function doSwitch() {
    const { data, validationError } = await switchProfile({
      profileId: profile.id,
    })
    toast({ description: data?.message ?? validationError?.profileId })
    if (data) router.replace("/")
  }

  return (
    <div className="space-y-1.5">
      {/* eslint-disable-next-line @next/next/no-img-element */}
      <img
        src={profile.profileImgPath}
        width="96"
        height="96"
        alt="profile-image"
        // eslint-disable-next-line @typescript-eslint/no-misused-promises
        onClick={doSwitch}
        className="cursor-pointer outline-1 hover:outline"
      />
      <h3 className="text-center">{profile.name}</h3>
    </div>
  )
}


================================================
FILE: src/app/api/(webhook)/stripe/route.ts
================================================
import type { Stripe } from "stripe"
import { stripe } from "~/lib/stripe"
import { NextResponse } from "next/server"
import { db } from "~/db/client"
import { accounts } from "~/db/schema"
import { eq } from "drizzle-orm"
import { headers } from "next/headers"
import { env } from "~/env.mjs"
import { planTuple } from "~/lib/configs"
import { z } from "zod"

export const runtime = "nodejs"

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

  let event: Stripe.Event

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

  if (event.type === "checkout.session.completed") {
    const session = event.data.object as Stripe.Checkout.Session
    // Retrieve the subscription details from Stripe.
    const subscription = await stripe.subscriptions.retrieve(
      session.subscription as string,
    )

    // Update the user stripe into in our database.
    const userId = session.metadata!.userId!
    const plan = session.metadata!.planName!
    const planSchema = z.enum(planTuple)
    const validatedPlan = planSchema.parse(plan)

    await db
      .update(accounts)
      .set({
        stripeCustomerId: subscription.customer as string,
        membership: validatedPlan,
      })
      .where(eq(accounts.id, userId))
  }

  if (event.type === "customer.subscription.updated") {
    const data = event.data.object as Stripe.Subscription
    if (data.canceled_at)
      await db
        .update(accounts)
        .set({ membership: "free" })
        .where(eq(accounts.stripeCustomerId, data.customer as string))
  }

  return NextResponse.json({ message: "Received" }, { status: 200 })
}


================================================
FILE: src/app/error.tsx
================================================
"use client"

import { useEffect } from "react"
import { Button } from "~/components/ui/button"

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  useEffect(() => {
    console.error(error)
  }, [error])

  return (
    <main className="container grid min-h-screen place-content-center space-y-5 text-center">
      <h1 className="text-3xl font-semibold">There was a problem</h1>
      <p>{error.message}</p>
      <section className="space-x-8">
        <Button onClick={() => reset()} className="font-semibold">
          Try again
        </Button>
        <Button asChild variant="secondary" className="font-semibold">
          <a href="/">Go back home</a>
        </Button>
      </section>
    </main>
  )
}


================================================
FILE: src/app/layout.tsx
================================================
import "~/lib/globals.css"
import { Inter } from "next/font/google"
import { cn } from "~/lib/utils"
import { ThemeProvider } from "~/components/theme-provider"
import { ClerkProvider } from "@clerk/nextjs"
import { Toaster } from "~/components/ui/toaster"
import { Analytics } from "@vercel/analytics/react"

export const runtime = "edge"
export const preferredRegion = "iad1"
const inter = Inter({ subsets: ["latin"] })

const siteConfig = {
  title: "Netflix Clone",
  description:
    "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).",
  url: "/",
  siteName: "Nextflix",
}
export const metadata = {
  metadataBase: new URL("https://nextflix-blush.vercel.app"),
  title: siteConfig.title,
  description: siteConfig.description,
  openGraph: {
    title: siteConfig.title,
    description: siteConfig.description,
    url: siteConfig.url,
    siteName: siteConfig.siteName,
    locale: "en_US",
    type: "website",
  },
  twitter: {
    card: "summary_large_image",
    title: siteConfig.title,
  },
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <ClerkProvider>
      <html lang="en" suppressHydrationWarning>
        <body
          className={cn(
            "bg-neutral-900 text-slate-50 antialiased scrollbar-none",
            inter.className,
          )}
        >
          <ThemeProvider attribute="class" defaultTheme="dark">
            {children}
          </ThemeProvider>
          <Toaster />
          <Analytics />
        </body>
      </html>
    </ClerkProvider>
  )
}


================================================
FILE: src/app/not-found.tsx
================================================
import Link from "next/link"
import { Button } from "~/components/ui/button"

export default function NotFound() {
  return (
    <main className="grid min-h-screen place-content-center space-y-5 text-center">
      <h2 className="text-3xl font-semibold">Not Found</h2>
      <p>Could not find requested resource</p>
      <Button asChild variant="outline" className="font-semibold">
        <Link href="/">Return Home</Link>
      </Button>
    </main>
  )
}


================================================
FILE: src/components/link-button.tsx
================================================
"use client"
import { useRouter } from "next/navigation"

export function LinkButton({
  children,
  href,
}: {
  children: React.ReactNode
  href: string
}) {
  const router = useRouter()
  return (
    <button
      onClick={() => {
        router.push(href)
        router.refresh()
      }}
    >
      {children}
    </button>
  )
}


================================================
FILE: src/components/modal-card.tsx
================================================
"use client"
import type { ShowWithVideoAndGenre } from "~/lib/types"
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "~/components/ui/card"
import { PlusCircle, CheckCircle } from "lucide-react"
import { toggleMyShow } from "~/actions"
import { useOptimisticAction } from "next-safe-action/hook"

interface ModalCardProps extends React.ComponentPropsWithoutRef<"div"> {
  show: ShowWithVideoAndGenre
  isSaved?: boolean
}
export function ModalCard({ show, isSaved, ...props }: ModalCardProps) {
  const trailer = show.videos.results.find((el) => el.type === "Trailer")
  return (
    <Card {...props}>
      <CardHeader>
        <CardTitle className="flex items-center gap-1.5">
          {show.title ?? show.name}
          {isSaved !== undefined && (
            <MyShowToggler show={show} isSaved={isSaved} />
          )}
        </CardTitle>
        <div className="flex items-center gap-1.5">
          <p className="text-green-400">
            {Math.round((show.vote_average * 100) / 10)}% Match
          </p>
          <p>
            {show.release_date?.substring(0, 4) ??
              show.first_air_date?.substring(0, 4)}
          </p>
          <p className="border border-neutral-500 px-1 text-xs text-white/50">
            EN
          </p>
        </div>
        <CardDescription>{show.overview}</CardDescription>
        <p className="text-left text-sm">
          {show.genres.map((genre) => genre.name).join(", ")}
        </p>
      </CardHeader>
      <CardContent className="p-0">
        {trailer ? (
          <iframe
            src={`https://www.youtube.com/embed/${trailer.key}`}
            className="aspect-video w-full rounded-lg"
          />
        ) : (
          <div className="grid aspect-video animate-pulse place-content-center text-xl font-semibold">
            No Trailer
          </div>
        )}
      </CardContent>
    </Card>
  )
}

function MyShowToggler({
  show,
  isSaved,
}: {
  show: ShowWithVideoAndGenre
  isSaved: boolean
}) {
  const {
    execute: executeToggle,
    optimisticData,
    res,
    isExecuting,
  } = useOptimisticAction(toggleMyShow, { isSaved })

  function toggle() {
    void executeToggle(
      {
        id: show.id,
        isSaved: res.data?.isSaved ?? isSaved,
        movieOrTv: show.title ? "movie" : "tv",
      },
      { isSaved: !res.data?.isSaved ?? !isSaved },
    )
  }

  return (
    <button onClick={toggle} disabled={isExecuting}>
      {optimisticData.isSaved ? (
        <CheckCircle
          className="h-6 w-6 cursor-pointer"
          strokeWidth="1.5"
          opacity={isExecuting ? 0.5 : 1}
        />
      ) : (
        <PlusCircle
          className="h-6 w-6 cursor-pointer"
          strokeWidth="1.5"
          opacity={isExecuting ? 0.5 : 1}
        />
      )}
    </button>
  )
}


================================================
FILE: src/components/overlay-scrollbar.tsx
================================================
"use client"
import { useOverlayScrollbars } from "overlayscrollbars-react"
import "overlayscrollbars/overlayscrollbars.css"
import { useEffect } from "react"

export function OverlayScrollbar() {
  const [initBodyOverlayScrollbars] = useOverlayScrollbars({
    defer: true,
    options: {
      scrollbars: {
        theme: "os-theme-light",
        autoHide: "scroll",
      },
    },
  })

  useEffect(() => {
    //only run on none touch screen devices
    if (window.matchMedia("(pointer: fine)").matches)
      initBodyOverlayScrollbars(document.body)
  }, [initBodyOverlayScrollbars])

  return null
}


================================================
FILE: src/components/show-bg.tsx
================================================
import { type Show } from "~/lib/types"
import Image from "next/image"

export function ShowBg({ show }: { show: Show }) {
  return (
    <div
      aria-label="background"
      className="absolute inset-0 -z-10 h-screen w-full"
    >
      <div className="h-full w-full bg-black/60 bg-gradient-to-b from-neutral-900/0 to-neutral-900" />
      <Image
        src={`https://image.tmdb.org/t/p/original/${show.backdrop_path}`}
        alt="background-image"
        className="-z-10 object-cover"
        fill
        priority
      />
    </div>
  )
}


================================================
FILE: src/components/show-carousel.tsx
================================================
"use client"
import { useSnapCarousel } from "react-snap-carousel"
import type { Show } from "~/lib/types"
import { useRef } from "react"
import { useDraggable } from "react-use-draggable-scroll"
import { Button } from "./ui/button"
import { ChevronLeft, ChevronRight } from "lucide-react"
import Link from "next/link"
import { type MutableRefObject, type RefCallback } from "react"

export function ShowsCarousel({
  title,
  shows,
}: {
  title: string
  shows: Show[]
}) {
  const { scrollRef, next, prev } = useSnapCarousel()

  const dragRef =
    useRef<HTMLDivElement>() as React.MutableRefObject<HTMLInputElement>
  const { events } = useDraggable(dragRef)

  return (
    <section>
      <div className="w-full max-w-screen-2xl space-y-1 sm:space-y-2.5">
        <h2 className="text-lg font-semibold md:text-xl">{title}</h2>
        <div className="group relative flex items-center">
          <Button
            aria-label="scroll left"
            variant="ghost"
            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"
            onClick={() => prev()}
          >
            <ChevronLeft className="h-8 w-8 text-white" aria-hidden="true" />
          </Button>
          <Button
            aria-label="scroll right"
            variant="ghost"
            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"
            onClick={() => next()}
          >
            <ChevronRight className="h-8 w-8 text-white" aria-hidden="true" />
          </Button>
          <div
            className="flex gap-1.5 overflow-auto py-2 scrollbar-none"
            ref={mergeRefs(dragRef, scrollRef)}
            {...events}
          >
            {shows.map((show) => (
              <Link
                href={`/show/${show.id}?mediaType=${
                  show.title ? "movie" : "tv"
                }`}
                scroll={false}
                key={show.id}
              >
                {/* eslint-disable-next-line @next/next/no-img-element */}
                <img
                  src={`https://image.tmdb.org/t/p/w300${
                    show.backdrop_path ?? show.poster_path
                  }`}
                  alt="show-backdrop"
                  width={240}
                  height={135}
                  className="aspect-video min-w-[160px] object-cover transition-transform hover:scale-110 md:min-w-[240px]"
                />
              </Link>
            ))}
          </div>
        </div>
      </div>
    </section>
  )
}

type MutableRefList<T> = Array<
  RefCallback<T> | MutableRefObject<T> | undefined | null
>
function mergeRefs<T>(...refs: MutableRefList<T>): RefCallback<T> {
  return (val: T) => {
    setRef(val, ...refs)
  }
}

function setRef<T>(val: T, ...refs: MutableRefList<T>): void {
  refs.forEach((ref) => {
    if (typeof ref === "function") {
      ref(val)
    } else if (ref != null) {
      ref.current = val
    }
  })
}


================================================
FILE: src/components/show-hero.tsx
================================================
import { type Show } from "~/lib/types"
import { Play, Info } from "lucide-react"
import Link from "next/link"
import { Button } from "~/components/ui/button"

export function ShowHero({ show }: { show: Show }) {
  return (
    <div className="flex min-h-[384px] max-w-lg flex-col justify-center space-y-3">
      <p className="text-3xl font-bold md:text-4xl">{show.title}</p>
      <div className="flex space-x-2 text-xs font-semibold md:text-sm">
        <p className="text-green-600">
          {Math.round((show.vote_average * 100) / 10)}% Match
        </p>
        <p>{show.release_date ?? show.first_air_date}</p>
      </div>
      <p className="line-clamp-4 text-sm text-gray-300 md:text-base">
        {show.overview}
      </p>
      <div className="flex items-center gap-3">
        <Link
          href={`/show/${show.id}?mediaType=${show.title ? "movie" : "tv"}`}
          scroll={false}
        >
          <Button className="flex gap-1.5">
            <Play fill="black" />
            Play
          </Button>
        </Link>
        <Link
          href={`/show/${show.id}?mediaType=${show.title ? "movie" : "tv"}`}
          scroll={false}
        >
          <Button variant="outline" className="flex gap-1.5">
            <Info />
            More Info
          </Button>
        </Link>
      </div>
    </div>
  )
}


================================================
FILE: src/components/theme-provider.tsx
================================================
"use client"
import { ThemeProvider as NextThemeProvider } from "next-themes"
import { type ThemeProviderProps } from "next-themes/dist/types"

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


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

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

const buttonVariants = cva(
  "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",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/90",
        destructive:
          "bg-destructive text-destructive-foreground hover:bg-destructive/90",
        outline:
          "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
        secondary:
          "bg-secondary text-secondary-foreground hover:bg-secondary/80",
        ghost: "hover:bg-accent hover:text-accent-foreground",
        link: "text-primary underline-offset-4 hover:underline",
      },
      size: {
        default: "h-10 px-4 py-2",
        sm: "h-9 rounded-md px-3",
        lg: "h-11 rounded-md px-8",
        icon: "h-10 w-10",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

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

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

export { Button, buttonVariants }


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

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

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

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

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

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

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

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

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


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

import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react"

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

const DropdownMenu = DropdownMenuPrimitive.Root

const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger

const DropdownMenuGroup = DropdownMenuPrimitive.Group

const DropdownMenuPortal = DropdownMenuPrimitive.Portal

const DropdownMenuSub = DropdownMenuPrimitive.Sub

const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup

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

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

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

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

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

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

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

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

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

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


================================================
FILE: src/components/ui/input.tsx
================================================
/* eslint-disable @typescript-eslint/no-empty-interface */
import * as React from "react"

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

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

const Input = React.forwardRef<HTMLInputElement, InputProps>(
  ({ className, type, ...props }, ref) => {
    return (
      <input
        type={type}
        className={cn(
          "flex h-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",
          className
        )}
        ref={ref}
        {...props}
      />
    )
  }
)
Input.displayName = "Input"

export { Input }


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

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

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

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

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

export { Label }


================================================
FILE: src/components/ui/scroll-area.tsx
================================================
"use client"

import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"

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

const ScrollArea = React.forwardRef<
  React.ElementRef<typeof ScrollAreaPrimitive.Root>,
  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
  <ScrollAreaPrimitive.Root
    ref={ref}
    className={cn("relative overflow-hidden", className)}
    {...props}
  >
    <ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
      {children}
    </ScrollAreaPrimitive.Viewport>
    <ScrollBar />
    <ScrollAreaPrimitive.Corner />
  </ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName

const ScrollBar = React.forwardRef<
  React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
  <ScrollAreaPrimitive.ScrollAreaScrollbar
    ref={ref}
    orientation={orientation}
    className={cn(
      "flex touch-none select-none transition-colors",
      orientation === "vertical" &&
        "h-full w-2.5 border-l border-l-transparent p-[1px]",
      orientation === "horizontal" &&
        "h-2.5 border-t border-t-transparent p-[1px]",
      className
    )}
    {...props}
  >
    <ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
  </ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName

export { ScrollArea, ScrollBar }


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

import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"

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

const Separator = React.forwardRef<
  React.ElementRef<typeof SeparatorPrimitive.Root>,
  React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
  (
    { className, orientation = "horizontal", decorative = true, ...props },
    ref
  ) => (
    <SeparatorPrimitive.Root
      ref={ref}
      decorative={decorative}
      orientation={orientation}
      className={cn(
        "shrink-0 bg-border",
        orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
        className
      )}
      {...props}
    />
  )
)
Separator.displayName = SeparatorPrimitive.Root.displayName

export { Separator }


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

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

export { Skeleton }


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

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

const ToastProvider = ToastPrimitives.Provider

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

const toastVariants = cva(
  "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",
  {
    variants: {
      variant: {
        default: "border bg-background",
        destructive:
          "destructive group border-destructive bg-destructive text-destructive-foreground",
      },
    },
    defaultVariants: {
      variant: "default",
    },
  }
)

const Toast = React.forwardRef<
  React.ElementRef<typeof ToastPrimitives.Root>,
  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
    VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
  return (
    <ToastPrimitives.Root
      ref={ref}
      className={cn(toastVariants({ variant }), className)}
      {...props}
    />
  )
})
Toast.displayName = ToastPrimitives.Root.displayName

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

const ToastClose = React.forwardRef<
  React.ElementRef<typeof ToastPrimitives.Close>,
  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
  <ToastPrimitives.Close
    ref={ref}
    className={cn(
      "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",
      className
    )}
    toast-close=""
    {...props}
  >
    <X className="h-4 w-4" />
  </ToastPrimitives.Close>
))
ToastClose.displayName = ToastPrimitives.Close.displayName

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

const ToastDescription = React.forwardRef<
  React.ElementRef<typeof ToastPrimitives.Description>,
  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
  <ToastPrimitives.Description
    ref={ref}
    className={cn("text-sm opacity-90", className)}
    {...props}
  />
))
ToastDescription.displayName = ToastPrimitives.Description.displayName

type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>

type ToastActionElement = React.ReactElement<typeof ToastAction>

export {
  type ToastProps,
  type ToastActionElement,
  ToastProvider,
  ToastViewport,
  Toast,
  ToastTitle,
  ToastDescription,
  ToastClose,
  ToastAction,
}


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

import {
  Toast,
  ToastClose,
  ToastDescription,
  ToastProvider,
  ToastTitle,
  ToastViewport,
} from "~/components/ui/toast"
import { useToast } from "~/components/ui/use-toast"

export function Toaster() {
  const { toasts } = useToast()

  return (
    <ToastProvider>
      {toasts.map(function ({ id, title, description, action, ...props }) {
        return (
          <Toast key={id} {...props}>
            <div className="grid gap-1">
              {title && <ToastTitle>{title}</ToastTitle>}
              {description && (
                <ToastDescription>{description}</ToastDescription>
              )}
            </div>
            {action}
            <ToastClose />
          </Toast>
        )
      })}
      <ToastViewport />
    </ToastProvider>
  )
}


================================================
FILE: src/components/ui/use-toast.ts
================================================
// Inspired by react-hot-toast library
import * as React from "react"

import type {
  ToastActionElement,
  ToastProps,
} from "~/components/ui/toast"

const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000

type ToasterToast = ToastProps & {
  id: string
  title?: React.ReactNode
  description?: React.ReactNode
  action?: ToastActionElement
}

const actionTypes = {
  ADD_TOAST: "ADD_TOAST",
  UPDATE_TOAST: "UPDATE_TOAST",
  DISMISS_TOAST: "DISMISS_TOAST",
  REMOVE_TOAST: "REMOVE_TOAST",
} as const

let count = 0

function genId() {
  count = (count + 1) % Number.MAX_VALUE
  return count.toString()
}

type ActionType = typeof actionTypes

type Action =
  | {
      type: ActionType["ADD_TOAST"]
      toast: ToasterToast
    }
  | {
      type: ActionType["UPDATE_TOAST"]
      toast: Partial<ToasterToast>
    }
  | {
      type: ActionType["DISMISS_TOAST"]
      toastId?: ToasterToast["id"]
    }
  | {
      type: ActionType["REMOVE_TOAST"]
      toastId?: ToasterToast["id"]
    }

interface State {
  toasts: ToasterToast[]
}

const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()

const addToRemoveQueue = (toastId: string) => {
  if (toastTimeouts.has(toastId)) {
    return
  }

  const timeout = setTimeout(() => {
    toastTimeouts.delete(toastId)
    dispatch({
      type: "REMOVE_TOAST",
      toastId: toastId,
    })
  }, TOAST_REMOVE_DELAY)

  toastTimeouts.set(toastId, timeout)
}

export const reducer = (state: State, action: Action): State => {
  switch (action.type) {
    case "ADD_TOAST":
      return {
        ...state,
        toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
      }

    case "UPDATE_TOAST":
      return {
        ...state,
        toasts: state.toasts.map((t) =>
          t.id === action.toast.id ? { ...t, ...action.toast } : t
        ),
      }

    case "DISMISS_TOAST": {
      const { toastId } = action

      // ! Side effects ! - This could be extracted into a dismissToast() action,
      // but I'll keep it here for simplicity
      if (toastId) {
        addToRemoveQueue(toastId)
      } else {
        state.toasts.forEach((toast) => {
          addToRemoveQueue(toast.id)
        })
      }

      return {
        ...state,
        toasts: state.toasts.map((t) =>
          t.id === toastId || toastId === undefined
            ? {
                ...t,
                open: false,
              }
            : t
        ),
      }
    }
    case "REMOVE_TOAST":
      if (action.toastId === undefined) {
        return {
          ...state,
          toasts: [],
        }
      }
      return {
        ...state,
        toasts: state.toasts.filter((t) => t.id !== action.toastId),
      }
  }
}

const listeners: Array<(state: State) => void> = []

let memoryState: State = { toasts: [] }

function dispatch(action: Action) {
  memoryState = reducer(memoryState, action)
  listeners.forEach((listener) => {
    listener(memoryState)
  })
}

type Toast = Omit<ToasterToast, "id">

function toast({ ...props }: Toast) {
  const id = genId()

  const update = (props: ToasterToast) =>
    dispatch({
      type: "UPDATE_TOAST",
      toast: { ...props, id },
    })
  const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })

  dispatch({
    type: "ADD_TOAST",
    toast: {
      ...props,
      id,
      open: true,
      onOpenChange: (open) => {
        if (!open) dismiss()
      },
    },
  })

  return {
    id: id,
    dismiss,
    update,
  }
}

function useToast() {
  const [state, setState] = React.useState<State>(memoryState)

  React.useEffect(() => {
    listeners.push(setState)
    return () => {
      const index = listeners.indexOf(setState)
      if (index > -1) {
        listeners.splice(index, 1)
      }
    }
  }, [state])

  return {
    ...state,
    toast,
    dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
  }
}

export { useToast, toast }


================================================
FILE: src/db/client.ts
================================================
import { env } from "~/env.mjs"
import * as schema from "./schema"
import { neon, neonConfig } from "@neondatabase/serverless"
import { drizzle } from "drizzle-orm/neon-http"

neonConfig.fetchConnectionCache = true

const sql = neon(env.DATABASE_URL)
export const db = drizzle(sql, { schema })


================================================
FILE: src/db/migrate.ts
================================================
import { drizzle, type PostgresJsDatabase } from "drizzle-orm/postgres-js"
import { migrate } from "drizzle-orm/postgres-js/migrator"
import postgres from "postgres"
import "dotenv/config"

const sql = postgres(process.env.DATABASE_URL!, {
  max: 1,
})
const db: PostgresJsDatabase = drizzle(sql)

await migrate(db, { migrationsFolder: "drizzle" })
console.log("migration completed")


================================================
FILE: src/db/schema.ts
================================================
import { relations } from "drizzle-orm"
import {
  pgTable,
  varchar,
  integer,
  timestamp,
  pgEnum,
  primaryKey,
  uniqueIndex,
  index,
  unique,
} from "drizzle-orm/pg-core"
import { planTuple } from "~/lib/configs"

export const membershipEnum = pgEnum("membership", planTuple)
export const accounts = pgTable(
  "accounts",
  {
    id: varchar("id", { length: 256 }).primaryKey(),
    createdAt: timestamp("createdAt").defaultNow().notNull(),
    email: varchar("email", { length: 256 }).notNull(),
    membership: membershipEnum("membership").notNull().default("free"),
    stripeCustomerId: varchar("stripe_customer_id", { length: 256 }),
    activeProfileId: varchar("active_profile_id", { length: 256 }).notNull(),
  },
  (table) => {
    return {
      activeProfileIdx: uniqueIndex("active_profile_idx").on(
        table.activeProfileId,
      ),
    }
  },
)
export const accountsRelations = relations(accounts, ({ many, one }) => ({
  profiles: many(profiles),
  activeProfile: one(profiles, {
    fields: [accounts.activeProfileId],
    references: [profiles.id],
  }),
}))

export const profiles = pgTable(
  "profiles",
  {
    id: varchar("id", { length: 256 }).primaryKey(),
    accountId: varchar("account_id", { length: 256 })
      .references(() => accounts.id, { onDelete: "cascade" })
      .notNull(),
    profileImgPath: varchar("profile_img_path", { length: 256 }).notNull(),
    name: varchar("name", { length: 256 }).notNull(),
  },
  (table) => {
    return {
      unq: unique().on(table.accountId, table.name),
      accountIdIdx: index("account_id_idx").on(table.accountId),
    }
  },
)
export const profilesRelation = relations(profiles, ({ one, many }) => ({
  ownerAccount: one(accounts, {
    fields: [profiles.accountId],
    references: [accounts.id],
  }),
  savedShows: many(myShows),
}))

export const mediaTypeEnum = pgEnum("media_type", ["movie", "tv"])
export const myShows = pgTable(
  "my_shows",
  {
    id: integer("id").notNull(),
    mediaType: mediaTypeEnum("media_type").notNull(),
    profileId: varchar("profile_id", { length: 256 })
      .references(() => profiles.id, { onDelete: "cascade" })
      .notNull(),
  },
  (table) => {
    return {
      profileIdIdx: index("profile_id_idx").on(table.profileId),
      pk: primaryKey(table.id, table.profileId),
    }
  },
)
export const myShowsRelation = relations(myShows, ({ one }) => ({
  profile: one(profiles, {
    fields: [myShows.profileId],
    references: [profiles.id],
  }),
}))


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

export const env = createEnv({
  /**
   * Specify your server-side environment variables schema here. This way you can ensure the app
   * isn't built with invalid env vars.
   */
  server: {
    NODE_ENV: z.enum(["development", "test", "production"]),

    DATABASE_URL: z.string().url(),
    CLERK_SECRET_KEY: z.string().min(1),
    STRIPE_SECRET_KEY: z.string().min(1),
    STRIPE_WEBHOOK_SECRET: z.string().min(1),
    STRIPE_DEV_WEBHOOK_SECRET: z.string().min(1),
  },

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

    NEXT_PUBLIC_TMDB_API: z.string().min(1),
    NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string().min(1),
    NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string().min(1),
  },

  /**
   * You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g.
   * middlewares) or client-side so we need to destruct manually.
   */
  runtimeEnv: {
    NODE_ENV: process.env.NODE_ENV,
    // NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,

    DATABASE_URL: process.env.DATABASE_URL,
    NEXT_PUBLIC_TMDB_API: process.env.NEXT_PUBLIC_TMDB_API,
    NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY:
      process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY,
    CLERK_SECRET_KEY: process.env.CLERK_SECRET_KEY,
    NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY:
      process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY,
    STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY,
    STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET,
    STRIPE_DEV_WEBHOOK_SECRET: process.env.STRIPE_DEV_WEBHOOK_SECRET,
  },
  /**
   * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation.
   * This is especially useful for Docker builds.
   */
  skipValidation: !!process.env.SKIP_ENV_VALIDATION,
})


================================================
FILE: src/lib/client-fetchers.ts
================================================
import { env } from "~/env.mjs"
import type { Show, MediaType } from "./types"
import { ERR } from "./utils"

export async function getShows(mediaType: MediaType) {
  const [
    trendingRes,
    topRatedRes,
    actionThrillerRes,
    comedyRes,
    horrorRes,
    romanceRes,
    documentaryRes,
  ] = await Promise.all([
    fetch(
      `https://api.themoviedb.org/3/trending/${mediaType}/week?api_key=${env.NEXT_PUBLIC_TMDB_API}`,
    ),
    fetch(
      `https://api.themoviedb.org/3/${mediaType}/top_rated?api_key=${env.NEXT_PUBLIC_TMDB_API}`,
    ),
    fetch(
      `https://api.themoviedb.org/3/discover/${mediaType}?api_key=${env.NEXT_PUBLIC_TMDB_API}&with_genres=28`,
    ),
    fetch(
      `https://api.themoviedb.org/3/discover/${mediaType}?api_key=${env.NEXT_PUBLIC_TMDB_API}&with_genres=35`,
    ),
    fetch(
      `https://api.themoviedb.org/3/discover/${mediaType}?api_key=${env.NEXT_PUBLIC_TMDB_API}&with_genres=27`,
    ),
    fetch(
      `https://api.themoviedb.org/3/discover/${mediaType}?api_key=${env.NEXT_PUBLIC_TMDB_API}&with_genres=10749`,
    ),
    fetch(
      `https://api.themoviedb.org/3/discover/${mediaType}?api_key=${env.NEXT_PUBLIC_TMDB_API}&with_genres=99`,
    ),
  ])

  if (
    !trendingRes.ok ||
    !topRatedRes.ok ||
    !actionThrillerRes.ok ||
    !comedyRes.ok ||
    !horrorRes.ok ||
    !romanceRes.ok ||
    !documentaryRes.ok
  )
    throw new Error(ERR.fetch)

  const [
    trending,
    topRated,
    actionThriller,
    comedy,
    horror,
    romance,
    documentary,
  ] = await Promise.all<{ results: Show[] }>([
    trendingRes.json(),
    topRatedRes.json(),
    actionThrillerRes.json(),
    comedyRes.json(),
    horrorRes.json(),
    romanceRes.json(),
    documentaryRes.json(),
  ])

  if (
    !trending ||
    !topRated ||
    !actionThriller ||
    !comedy ||
    !horror ||
    !romance ||
    !documentary
  )
    throw new Error(ERR.fetch)

  return {
    trending: trending.results,
    topRated: topRated.results,
    actionThriller: actionThriller.results,
    comedy: comedy.results,
    horror: horror.results,
    romance: romance.results,
    documentary: documentary.results,
  }
}


================================================
FILE: src/lib/configs.ts
================================================
export const PLANS = [
  {
    id: "0",
    name: "free",
    price: 0,
    description: "Free Video Stream",
  },
  {
    id: "price_1NeEIZBrDYSkolG5bjbEmccv",
    name: "basic",
    price: 5,
    description: "Basic Video Stream",
  },
  {
    id: "price_1NeE60BrDYSkolG5PidpmNHW",
    name: "standard",
    price: 10,
    description: "Standard Video Stream",
  },
  {
    id: "price_1Nd71XBrDYSkolG53ADLYQXk",
    name: "premium",
    price: 20,
    description: "Premium Video Stream",
  },
] as const

const tupleMap = <
  const T extends readonly Record<string, unknown>[],
  Key extends keyof T[number],
>(
  input: T,
  key: Key,
): { [k in keyof T]: T[k][Key] } => {
  return input.map((row) => row[key as never]) as never
}
export const planTuple = tupleMap(PLANS, "name")


================================================
FILE: src/lib/globals.css
================================================
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  :root {
    --background: 0 0% 100%;
    --foreground: 0 0% 3.9%;

    --muted: 0 0% 96.1%;
    --muted-foreground: 0 0% 45.1%;

    --popover: 0 0% 100%;
    --popover-foreground: 0 0% 3.9%;

    --card: 0 0% 100%;
    --card-foreground: 0 0% 3.9%;

    --border: 0 0% 89.8%;
    --input: 0 0% 89.8%;

    --primary: 0 0% 9%;
    --primary-foreground: 0 0% 98%;

    --secondary: 0 0% 96.1%;
    --secondary-foreground: 0 0% 9%;

    --accent: 0 0% 96.1%;
    --accent-foreground: 0 0% 9%;

    --destructive: 0 84.2% 60.2%;
    --destructive-foreground: 0 0% 98%;

    --ring: 0 0% 63.9%;

    --radius: 0.5rem;
  }

  .dark {
    --background: 0 0% 3.9%;
    --foreground: 0 0% 98%;

    --muted: 0 0% 14.9%;
    --muted-foreground: 0 0% 63.9%;

    --popover: 0 0% 3.9%;
    --popover-foreground: 0 0% 98%;

    --card: 0 0% 3.9%;
    --card-foreground: 0 0% 98%;

    --border: 0 0% 14.9%;
    --input: 0 0% 14.9%;

    --primary: 0 0% 98%;
    --primary-foreground: 0 0% 9%;

    --secondary: 0 0% 14.9%;
    --secondary-foreground: 0 0% 98%;

    --accent: 0 0% 14.9%;
    --accent-foreground: 0 0% 98%;

    --destructive: 0 62.8% 30.6%;
    --destructive-foreground: 0 85.7% 97.3%;

    --ring: 0 0% 14.9%;
  }
}

@layer base {
  * {
    @apply border-border;
  }
  body {
    @apply bg-background text-foreground;
  }
}


================================================
FILE: src/lib/server-fetchers.ts
================================================
import { env } from "~/env.mjs"
import type { Show, MyShow, ShowWithVideoAndGenre } from "~/lib/types"
import { ERR } from "~/lib/utils"
import { db } from "~/db/client"
import { eq } from "drizzle-orm"
import { accounts, profiles, myShows } from "~/db/schema"
import { auth } from "@clerk/nextjs"
import type { MediaType } from "~/lib/types"

export async function getAccount() {
  const userId = auth().userId
  if (!userId) throw new Error(ERR.unauthenticated)
  const account = await db.query.accounts.findFirst({
    where: eq(accounts.id, userId),
  })
  if (!account) throw new Error(ERR.db)
  return account
}

export async function getAccountWithActiveProfile() {
  const userId = auth().userId
  if (!userId) throw new Error(ERR.unauthenticated)
  const account = await db.query.accounts.findFirst({
    where: eq(accounts.id, userId),
    columns: { activeProfileId: true },
    with: {
      activeProfile: true,
    },
  })
  if (!account) throw new Error(ERR.db)
  return account
}

export async function getAccountWithProfiles() {
  const userId = auth().userId
  if (!userId) throw new Error(ERR.unauthenticated)
  const account = await db.query.accounts.findFirst({
    where: eq(accounts.id, userId),
    with: {
      profiles: true,
    },
  })
  if (!account) throw new Error(ERR.db)
  return account
}

export async function getProfile(profileId: string) {
  const profile = await db.query.profiles.findFirst({
    where: eq(profiles.id, profileId),
  })
  if (!profile) throw new Error(ERR.db)
  return profile
}

export async function getMyShows(limit: number) {
  const account = await getAccountWithActiveProfile()
  const shows = await db.query.myShows.findMany({
    where: eq(myShows.profileId, account.activeProfileId),
    limit: limit + 1,
  })
  const hasNextPage = shows.length > limit ? true : false
  if (hasNextPage) shows.pop()
  const filteredShows = await getMyShowsFromTmdb(shows)
  return { shows: filteredShows, hasNextPage }
}

export async function getMyShowsFromTmdb(shows: MyShow[]) {
  const data = await Promise.all<Show | null>(
    shows.map(async (show) => {
      const res = await fetch(
        `https://api.themoviedb.org/3/${show.mediaType}/${show.id}?api_key=${env.NEXT_PUBLIC_TMDB_API}`,
      )
      if (!res.ok) return null
      return res.json()
    }),
  )
  const filteredShows = data.filter((el): el is Show => !!el)
  return filteredShows
}

export async function getShowVideoAndGenreWithStatus(
  showId: number,
  mediaType: MediaType,
) {
  const userId = auth().userId
  const accountPromise = userId
    ? db.query.accounts.findFirst({
        where: eq(accounts.id, userId),
        columns: {},
        with: {
          activeProfile: {
            columns: {},
            with: {
              savedShows: {
                where: eq(myShows.id, showId),
                limit: 1,
              },
            },
          },
        },
      })
    : null
  const [show, account] = await Promise.all([
    fetch(
      `https://api.themoviedb.org/3/${mediaType}/${showId}?api_key=${env.NEXT_PUBLIC_TMDB_API}&append_to_response=videos,genres`,
    )
      .then((r) => r.json() as Promise<ShowWithVideoAndGenre>)
      .catch((err) => console.error(err)),
    accountPromise,
  ])
  if (!show || account === undefined) throw new Error(ERR.fetch)
  const isSaved = account
    ? !!account.activeProfile.savedShows.length
    : undefined

  return { show, isSaved }
}


================================================
FILE: src/lib/stripe.ts
================================================
import Stripe from "stripe"
import { env } from "~/env.mjs"

export const stripe = new Stripe(env.STRIPE_SECRET_KEY, {
  apiVersion: "2023-08-16",
  typescript: true,
})


================================================
FILE: src/lib/types.ts
================================================
import { type myShows, type profiles, mediaTypeEnum } from "~/db/schema"
import type { PLANS } from "~/lib/configs"

export type MyShow = typeof myShows.$inferSelect
export const MediaTuple = mediaTypeEnum.enumValues
export type MediaType = (typeof MediaTuple)[number]
export type Profile = typeof profiles.$inferSelect
export type SubscriptionPlan = (typeof PLANS)[number]
export type PlanName = (typeof PLANS)[number]["name"]

export interface Show {
  id: number
  backdrop_path: string
  poster_path: string
  title?: string
  name?: string
  overview: string
  vote_average: number
  popularity: number
  release_date?: string
  first_air_date?: string
}
export interface ShowWithVideoAndGenre extends Show {
  videos: {
    results: Video[]
  }
  genres: Genre[]
}

type Video = {
  key: string
  type: string
}

type Genre = {
  id: number
  name: string
}

// "adult": false,
// "backdrop_path": "/e2Jd0sYMCe6qvMbswGQbM0Mzxt0.jpg",
// "genre_ids": [
//   28,
//   80,
//   53
// ],
// "id": 385687,
// "original_language": "en",
// "original_title": "Fast X",
// "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.",
// "popularity": 4654.279,
// "poster_path": "/fiVW06jE7z9YnO4trhaMEdclSiC.jpg",
// "release_date": "2023-05-17",
// "title": "Fast X",
// "video": false,
// "vote_average": 7.3,
// "vote_count": 2093


================================================
FILE: src/lib/utils.ts
================================================
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
import type { Show } from "./types"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

export const ERR = {
  unauthenticated: "Unauthenticated",
  unauthorized: "Unauthorized",
  db: "Failed to find in database",
  undefined: "Undefined variable",
  fetch: "Failed to fetch data",
  not_allowed: "User should not be allowed to do this action",
}

export function pickRandomShow(shows: Show[]) {
  const show = shows[Math.floor(Math.random() * shows.length)]
  if (show) return show
  else throw new Error(ERR.undefined)
}


================================================
FILE: src/middleware.ts
================================================
import { authMiddleware } from "@clerk/nextjs"
export default authMiddleware({
  publicRoutes: [
    "/",
    "/api/(.*)",
    "/show/(.*)",
    "/tv-shows",
    "/movies",
    "/new-and-popular",
    "/search(.*)",
  ],
})

export const config = {
  matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
}


================================================
FILE: tailwind.config.ts
================================================
/** @type {import('tailwindcss').Config} */
module.exports = {
  darkMode: ["class"],
  content: [
    "./pages/**/*.{ts,tsx}",
    "./components/**/*.{ts,tsx}",
    "./app/**/*.{ts,tsx}",
    "./src/**/*.{ts,tsx}",
  ],
  theme: {
    container: {
      center: true,
      padding: "2rem",
      screens: {
        "2xl": "1400px",
      },
    },
    extend: {
      colors: {
        border: "hsl(var(--border))",
        input: "hsl(var(--input))",
        ring: "hsl(var(--ring))",
        background: "hsl(var(--background))",
        foreground: "hsl(var(--foreground))",
        primary: {
          DEFAULT: "hsl(var(--primary))",
          foreground: "hsl(var(--primary-foreground))",
        },
        secondary: {
          DEFAULT: "hsl(var(--secondary))",
          foreground: "hsl(var(--secondary-foreground))",
        },
        destructive: {
          DEFAULT: "hsl(var(--destructive))",
          foreground: "hsl(var(--destructive-foreground))",
        },
        muted: {
          DEFAULT: "hsl(var(--muted))",
          foreground: "hsl(var(--muted-foreground))",
        },
        accent: {
          DEFAULT: "hsl(var(--accent))",
          foreground: "hsl(var(--accent-foreground))",
        },
        popover: {
          DEFAULT: "hsl(var(--popover))",
          foreground: "hsl(var(--popover-foreground))",
        },
        card: {
          DEFAULT: "hsl(var(--card))",
          foreground: "hsl(var(--card-foreground))",
        },
      },
      borderRadius: {
        lg: "var(--radius)",
        md: "calc(var(--radius) - 2px)",
        sm: "calc(var(--radius) - 4px)",
      },
      keyframes: {
        "accordion-down": {
          from: { height: 0 },
          to: { height: "var(--radix-accordion-content-height)" },
        },
        "accordion-up": {
          from: { height: "var(--radix-accordion-content-height)" },
          to: { height: 0 },
        },
      },
      animation: {
        "accordion-down": "accordion-down 0.2s ease-out",
        "accordion-up": "accordion-up 0.2s ease-out",
      },
      screens: {
        mobile: { raw: "(pointer: coarse)" },
      },
    },
  },
  plugins: [
    require("tailwindcss-animate"),
    // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-var-requires
    require("tailwind-scrollbar")({ nocompatible: true }),
  ],
}


================================================
FILE: tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "es2017",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "checkJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "noUncheckedIndexedAccess": true,
    "baseUrl": ".",
    "paths": {
      "~/*": ["./src/*"],
      "@/*": ["./*"]
    },
    "plugins": [
      {
        "name": "next"
      }
    ]
  },
  "include": [
    ".eslintrc.cjs",
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx",
    "**/*.cjs",
    "**/*.mjs",
    ".next/types/**/*.ts"
  ],
  "exclude": ["node_modules"]
}
Download .txt
gitextract_w94r5yd9/

├── .eslintrc.cjs
├── .github/
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── README.md
├── components.json
├── drizzle.config.ts
├── next.config.mjs
├── package.json
├── postcss.config.cjs
├── prettier.config.cjs
├── src/
│   ├── actions/
│   │   ├── index.ts
│   │   └── safe-action-client.ts
│   ├── app/
│   │   ├── (auth)/
│   │   │   ├── layout.tsx
│   │   │   ├── sign-in/
│   │   │   │   └── [[...sign-in]]/
│   │   │   │       └── page.tsx
│   │   │   └── sign-up/
│   │   │       └── [[...sign-up]]/
│   │   │           └── page.tsx
│   │   ├── (main)/
│   │   │   ├── @modal/
│   │   │   │   ├── (.)show/
│   │   │   │   │   └── [id]/
│   │   │   │   │       ├── modal.tsx
│   │   │   │   │       └── page.tsx
│   │   │   │   ├── default.tsx
│   │   │   │   └── loading.tsx
│   │   │   ├── account/
│   │   │   │   ├── loading.tsx
│   │   │   │   └── page.tsx
│   │   │   ├── default.tsx
│   │   │   ├── layout.tsx
│   │   │   ├── loading.tsx
│   │   │   ├── movies/
│   │   │   │   └── page.tsx
│   │   │   ├── my-list/
│   │   │   │   ├── infinite-scroller.tsx
│   │   │   │   ├── loading.tsx
│   │   │   │   └── page.tsx
│   │   │   ├── new-and-popular/
│   │   │   │   └── page.tsx
│   │   │   ├── page.tsx
│   │   │   ├── search/
│   │   │   │   ├── loading.tsx
│   │   │   │   ├── page.tsx
│   │   │   │   └── search-input.tsx
│   │   │   ├── show/
│   │   │   │   └── [id]/
│   │   │   │       └── page.tsx
│   │   │   ├── subscription/
│   │   │   │   ├── loading.tsx
│   │   │   │   ├── page.tsx
│   │   │   │   ├── plan-selector.tsx
│   │   │   │   └── result/
│   │   │   │       └── page.tsx
│   │   │   └── tv-shows/
│   │   │       └── page.tsx
│   │   ├── (profile)/
│   │   │   ├── loading.tsx
│   │   │   ├── manage-profile/
│   │   │   │   ├── [...slug]/
│   │   │   │   │   └── page.tsx
│   │   │   │   ├── add/
│   │   │   │   │   └── page.tsx
│   │   │   │   └── page.tsx
│   │   │   └── switch-profile/
│   │   │       ├── page.tsx
│   │   │       └── profile-switcher.tsx
│   │   ├── api/
│   │   │   └── (webhook)/
│   │   │       └── stripe/
│   │   │           └── route.ts
│   │   ├── error.tsx
│   │   ├── layout.tsx
│   │   └── not-found.tsx
│   ├── components/
│   │   ├── link-button.tsx
│   │   ├── modal-card.tsx
│   │   ├── overlay-scrollbar.tsx
│   │   ├── show-bg.tsx
│   │   ├── show-carousel.tsx
│   │   ├── show-hero.tsx
│   │   ├── theme-provider.tsx
│   │   └── ui/
│   │       ├── button.tsx
│   │       ├── card.tsx
│   │       ├── dropdown-menu.tsx
│   │       ├── input.tsx
│   │       ├── label.tsx
│   │       ├── scroll-area.tsx
│   │       ├── separator.tsx
│   │       ├── skeleton.tsx
│   │       ├── toast.tsx
│   │       ├── toaster.tsx
│   │       └── use-toast.ts
│   ├── db/
│   │   ├── client.ts
│   │   ├── migrate.ts
│   │   └── schema.ts
│   ├── env.mjs
│   ├── lib/
│   │   ├── client-fetchers.ts
│   │   ├── configs.ts
│   │   ├── globals.css
│   │   ├── server-fetchers.ts
│   │   ├── stripe.ts
│   │   ├── types.ts
│   │   └── utils.ts
│   └── middleware.ts
├── tailwind.config.ts
└── tsconfig.json
Download .txt
SYMBOL INDEX (96 symbols across 55 files)

FILE: src/app/(auth)/layout.tsx
  function AuthLayout (line 1) | function AuthLayout({

FILE: src/app/(auth)/sign-in/[[...sign-in]]/page.tsx
  function Page (line 3) | function Page() {

FILE: src/app/(auth)/sign-up/[[...sign-up]]/page.tsx
  function Page (line 3) | function Page() {

FILE: src/app/(main)/@modal/(.)show/[id]/modal.tsx
  function Modal (line 7) | function Modal({

FILE: src/app/(main)/@modal/(.)show/[id]/page.tsx
  function ShowModal (line 5) | async function ShowModal(props: {

FILE: src/app/(main)/@modal/default.tsx
  function Default (line 1) | function Default() {

FILE: src/app/(main)/@modal/loading.tsx
  function Loading (line 3) | function Loading() {

FILE: src/app/(main)/account/loading.tsx
  function Loading (line 3) | function Loading() {

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

FILE: src/app/(main)/default.tsx
  function Default (line 1) | function Default() {

FILE: src/app/(main)/layout.tsx
  function ShowsLayout (line 40) | function ShowsLayout({
  constant NAVINFO (line 58) | const NAVINFO = [
  function Header (line 74) | function Header() {
  function CustomeUserButton (line 124) | async function CustomeUserButton() {
  function MainMenu (line 181) | function MainMenu() {
  function Footer (line 217) | function Footer() {
  function createAccountAndProfile (line 271) | async function createAccountAndProfile() {

FILE: src/app/(main)/loading.tsx
  function Loading (line 3) | function Loading() {

FILE: src/app/(main)/movies/page.tsx
  function Movies (line 7) | async function Movies() {

FILE: src/app/(main)/my-list/infinite-scroller.tsx
  function ShowScroller (line 10) | function ShowScroller({

FILE: src/app/(main)/my-list/loading.tsx
  function Loading (line 3) | function Loading() {

FILE: src/app/(main)/my-list/page.tsx
  function MyShowPage (line 4) | async function MyShowPage() {

FILE: src/app/(main)/new-and-popular/page.tsx
  function NewAndPopular (line 9) | async function NewAndPopular() {
  function getNewAndPopularShows (line 41) | async function getNewAndPopularShows() {

FILE: src/app/(main)/page.tsx
  function Home (line 7) | async function Home() {

FILE: src/app/(main)/search/loading.tsx
  function Loading (line 3) | function Loading() {

FILE: src/app/(main)/search/page.tsx
  function SearchPage (line 7) | async function SearchPage({
  function searchShows (line 48) | async function searchShows(query: string) {

FILE: src/app/(main)/search/search-input.tsx
  type PageProps (line 7) | interface PageProps extends React.HTMLAttributes<HTMLElement> {
  function SearchInput (line 10) | function SearchInput({ initialQuery, ...props }: PageProps) {

FILE: src/app/(main)/show/[id]/page.tsx
  function ShowPage (line 5) | async function ShowPage(props: {

FILE: src/app/(main)/subscription/loading.tsx
  function Loading (line 3) | function Loading() {

FILE: src/app/(main)/subscription/page.tsx
  function SubscriptionPage (line 5) | async function SubscriptionPage() {

FILE: src/app/(main)/subscription/plan-selector.tsx
  function PlanSelector (line 9) | function PlanSelector({

FILE: src/app/(main)/subscription/result/page.tsx
  function ResultPage (line 5) | async function ResultPage({

FILE: src/app/(main)/tv-shows/page.tsx
  function TvShows (line 7) | async function TvShows() {

FILE: src/app/(profile)/loading.tsx
  function Loading (line 3) | function Loading() {

FILE: src/app/(profile)/manage-profile/[...slug]/page.tsx
  function ProfilePage (line 12) | function ProfilePage({

FILE: src/app/(profile)/manage-profile/add/page.tsx
  function AddProfilePage (line 12) | function AddProfilePage() {

FILE: src/app/(profile)/manage-profile/page.tsx
  function ManageProfilePage (line 6) | async function ManageProfilePage() {

FILE: src/app/(profile)/switch-profile/page.tsx
  function SwitchProfilePage (line 7) | async function SwitchProfilePage() {

FILE: src/app/(profile)/switch-profile/profile-switcher.tsx
  function ProfileSwitcher (line 7) | function ProfileSwitcher({ profile }: { profile: Profile }) {

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

FILE: src/app/error.tsx
  function Error (line 6) | function Error({

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

FILE: src/app/not-found.tsx
  function NotFound (line 4) | function NotFound() {

FILE: src/components/link-button.tsx
  function LinkButton (line 4) | function LinkButton({

FILE: src/components/modal-card.tsx
  type ModalCardProps (line 14) | interface ModalCardProps extends React.ComponentPropsWithoutRef<"div"> {
  function ModalCard (line 18) | function ModalCard({ show, isSaved, ...props }: ModalCardProps) {
  function MyShowToggler (line 62) | function MyShowToggler({

FILE: src/components/overlay-scrollbar.tsx
  function OverlayScrollbar (line 6) | function OverlayScrollbar() {

FILE: src/components/show-bg.tsx
  function ShowBg (line 4) | function ShowBg({ show }: { show: Show }) {

FILE: src/components/show-carousel.tsx
  function ShowsCarousel (line 11) | function ShowsCarousel({
  type MutableRefList (line 77) | type MutableRefList<T> = Array<
  function mergeRefs (line 80) | function mergeRefs<T>(...refs: MutableRefList<T>): RefCallback<T> {
  function setRef (line 86) | function setRef<T>(val: T, ...refs: MutableRefList<T>): void {

FILE: src/components/show-hero.tsx
  function ShowHero (line 6) | function ShowHero({ show }: { show: Show }) {

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

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

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

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

FILE: src/components/ui/toast.tsx
  type ToastProps (line 113) | type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
  type ToastActionElement (line 115) | type ToastActionElement = React.ReactElement<typeof ToastAction>

FILE: src/components/ui/toaster.tsx
  function Toaster (line 13) | function Toaster() {

FILE: src/components/ui/use-toast.ts
  constant TOAST_LIMIT (line 9) | const TOAST_LIMIT = 1
  constant TOAST_REMOVE_DELAY (line 10) | const TOAST_REMOVE_DELAY = 1000000
  type ToasterToast (line 12) | type ToasterToast = ToastProps & {
  function genId (line 28) | function genId() {
  type ActionType (line 33) | type ActionType = typeof actionTypes
  type Action (line 35) | type Action =
  type State (line 53) | interface State {
  function dispatch (line 134) | function dispatch(action: Action) {
  type Toast (line 141) | type Toast = Omit<ToasterToast, "id">
  function toast (line 143) | function toast({ ...props }: Toast) {
  function useToast (line 172) | function useToast() {

FILE: src/lib/client-fetchers.ts
  function getShows (line 5) | async function getShows(mediaType: MediaType) {

FILE: src/lib/configs.ts
  constant PLANS (line 1) | const PLANS = [

FILE: src/lib/server-fetchers.ts
  function getAccount (line 10) | async function getAccount() {
  function getAccountWithActiveProfile (line 20) | async function getAccountWithActiveProfile() {
  function getAccountWithProfiles (line 34) | async function getAccountWithProfiles() {
  function getProfile (line 47) | async function getProfile(profileId: string) {
  function getMyShows (line 55) | async function getMyShows(limit: number) {
  function getMyShowsFromTmdb (line 67) | async function getMyShowsFromTmdb(shows: MyShow[]) {
  function getShowVideoAndGenreWithStatus (line 81) | async function getShowVideoAndGenreWithStatus(

FILE: src/lib/types.ts
  type MyShow (line 4) | type MyShow = typeof myShows.$inferSelect
  type MediaType (line 6) | type MediaType = (typeof MediaTuple)[number]
  type Profile (line 7) | type Profile = typeof profiles.$inferSelect
  type SubscriptionPlan (line 8) | type SubscriptionPlan = (typeof PLANS)[number]
  type PlanName (line 9) | type PlanName = (typeof PLANS)[number]["name"]
  type Show (line 11) | interface Show {
  type ShowWithVideoAndGenre (line 23) | interface ShowWithVideoAndGenre extends Show {
  type Video (line 30) | type Video = {
  type Genre (line 35) | type Genre = {

FILE: src/lib/utils.ts
  function cn (line 5) | function cn(...inputs: ClassValue[]) {
  constant ERR (line 9) | const ERR = {
  function pickRandomShow (line 18) | function pickRandomShow(shows: Show[]) {
Condensed preview — 81 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (140K chars).
[
  {
    "path": ".eslintrc.cjs",
    "chars": 869,
    "preview": "/** @type {import(\"eslint\").Linter.Config} */\nconst config = {\n  parser: \"@typescript-eslint/parser\",\n  parserOptions: {"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 505,
    "preview": "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"
  },
  {
    "path": ".gitignore",
    "chars": 591,
    "preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
  },
  {
    "path": "README.md",
    "chars": 16819,
    "preview": "## Project using bleeding-edge stack. Drizzle ORM + Neon postgres + Clerk auth + Shadcn/ui + everything new in Next.js 1"
  },
  {
    "path": "components.json",
    "chars": 332,
    "preview": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"default\",\n  \"rsc\": true,\n  \"tsx\": true,\n  \"tailwind\": {\n"
  },
  {
    "path": "drizzle.config.ts",
    "chars": 304,
    "preview": "import type { Config } from \"drizzle-kit\"\nimport \"dotenv/config\"\n\nexport default {\n  schema: \"./src/db/schema.ts\",\n  out"
  },
  {
    "path": "next.config.mjs",
    "chars": 477,
    "preview": "/**\n * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful\n * for Docker b"
  },
  {
    "path": "package.json",
    "chars": 2173,
    "preview": "{\n  \"name\": \"nextflix\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"build\": \"next build\",\n    \"dev\": \"n"
  },
  {
    "path": "postcss.config.cjs",
    "chars": 107,
    "preview": "const config = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n};\n\nmodule.exports = config;\n"
  },
  {
    "path": "prettier.config.cjs",
    "chars": 163,
    "preview": "/** @type {import(\"prettier\").Config} */\nconst config = {\n  plugins: [require.resolve(\"prettier-plugin-tailwindcss\")],\n "
  },
  {
    "path": "src/actions/index.ts",
    "chars": 5420,
    "preview": "\"use server\"\nimport { z } from \"zod\"\nimport { authAction } from \"./safe-action-client\"\nimport { db } from \"~/db/client\"\n"
  },
  {
    "path": "src/actions/safe-action-client.ts",
    "chars": 441,
    "preview": "/* eslint-disable @typescript-eslint/require-await */\nimport { createSafeActionClient } from \"next-safe-action\"\nimport {"
  },
  {
    "path": "src/app/(auth)/layout.tsx",
    "chars": 205,
    "preview": "export default function AuthLayout({\n  children,\n}: {\n  children: React.ReactNode\n}) {\n  return (\n    <div className=\"gr"
  },
  {
    "path": "src/app/(auth)/sign-in/[[...sign-in]]/page.tsx",
    "chars": 95,
    "preview": "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",
    "chars": 95,
    "preview": "import { SignUp } from \"@clerk/nextjs\"\n\nexport default function Page() {\n  return <SignUp />\n}\n"
  },
  {
    "path": "src/app/(main)/@modal/(.)show/[id]/modal.tsx",
    "chars": 955,
    "preview": "\"use client\"\nimport type { ShowWithVideoAndGenre } from \"~/lib/types\"\nimport { useRef, useEffect } from \"react\"\nimport {"
  },
  {
    "path": "src/app/(main)/@modal/(.)show/[id]/page.tsx",
    "chars": 446,
    "preview": "import { Modal } from \"./modal\"\nimport type { MediaType } from \"~/lib/types\"\nimport { getShowVideoAndGenreWithStatus } f"
  },
  {
    "path": "src/app/(main)/@modal/default.tsx",
    "chars": 52,
    "preview": "export default function Default() {\n  return null\n}\n"
  },
  {
    "path": "src/app/(main)/@modal/loading.tsx",
    "chars": 293,
    "preview": "import { Skeleton } from \"~/components/ui/skeleton\"\n\nexport default function Loading() {\n  return (\n    <div className=\""
  },
  {
    "path": "src/app/(main)/account/loading.tsx",
    "chars": 218,
    "preview": "import { Skeleton } from \"~/components/ui/skeleton\"\n\nexport default function Loading() {\n  return (\n    <main className="
  },
  {
    "path": "src/app/(main)/account/page.tsx",
    "chars": 2778,
    "preview": "import { CreditCard, ChevronRight } from \"lucide-react\"\nimport { Button } from \"~/components/ui/button\"\nimport Link from"
  },
  {
    "path": "src/app/(main)/default.tsx",
    "chars": 52,
    "preview": "export default function Default() {\n  return null\n}\n"
  },
  {
    "path": "src/app/(main)/layout.tsx",
    "chars": 9100,
    "preview": "import Image from \"next/image\"\nimport Link from \"next/link\"\nimport { currentUser, SignedOut, auth, SignOutButton } from "
  },
  {
    "path": "src/app/(main)/loading.tsx",
    "chars": 444,
    "preview": "import { Skeleton } from \"~/components/ui/skeleton\"\n\nexport default function Loading() {\n  return (\n    <main>\n      <di"
  },
  {
    "path": "src/app/(main)/movies/page.tsx",
    "chars": 1130,
    "preview": "import { ShowsCarousel } from \"~/components/show-carousel\"\nimport { getShows } from \"~/lib/client-fetchers\"\nimport { Sho"
  },
  {
    "path": "src/app/(main)/my-list/infinite-scroller.tsx",
    "chars": 4292,
    "preview": "\"use client\"\nimport { useEffect, useRef, useState } from \"react\"\nimport type { Show } from \"~/lib/types\"\nimport { getMyS"
  },
  {
    "path": "src/app/(main)/my-list/loading.tsx",
    "chars": 189,
    "preview": "import { Skeleton } from \"~/components/ui/skeleton\"\n\nexport default function Loading() {\n  return (\n    <main className="
  },
  {
    "path": "src/app/(main)/my-list/page.tsx",
    "chars": 682,
    "preview": "import { ShowScroller } from \"./infinite-scroller\"\nimport { getMyShows } from \"~/lib/server-fetchers\"\n\nexport default as"
  },
  {
    "path": "src/app/(main)/new-and-popular/page.tsx",
    "chars": 2459,
    "preview": "import type { Show } from \"~/lib/types\"\nimport { ShowsCarousel } from \"~/components/show-carousel\"\nimport { ERR } from \""
  },
  {
    "path": "src/app/(main)/page.tsx",
    "chars": 1114,
    "preview": "import { getShows } from \"~/lib/client-fetchers\"\nimport { ShowHero } from \"~/components/show-hero\"\nimport { ShowBg } fro"
  },
  {
    "path": "src/app/(main)/search/loading.tsx",
    "chars": 189,
    "preview": "import { Skeleton } from \"~/components/ui/skeleton\"\n\nexport default function Loading() {\n  return (\n    <main className="
  },
  {
    "path": "src/app/(main)/search/page.tsx",
    "chars": 1876,
    "preview": "import { ERR } from \"~/lib/utils\"\nimport { env } from \"~/env.mjs\"\nimport type { Show } from \"~/lib/types\"\nimport { Searc"
  },
  {
    "path": "src/app/(main)/search/search-input.tsx",
    "chars": 800,
    "preview": "\"use client\"\nimport { Input } from \"~/components/ui/input\"\nimport { useEffect, useState } from \"react\"\nimport { useDebou"
  },
  {
    "path": "src/app/(main)/show/[id]/page.tsx",
    "chars": 587,
    "preview": "import { ModalCard } from \"~/components/modal-card\"\nimport type { MediaType } from \"~/lib/types\"\nimport { getShowVideoAn"
  },
  {
    "path": "src/app/(main)/subscription/loading.tsx",
    "chars": 176,
    "preview": "import { Skeleton } from \"~/components/ui/skeleton\"\n\nexport default function Loading() {\n  return (\n    <main>\n      <Sk"
  },
  {
    "path": "src/app/(main)/subscription/page.tsx",
    "chars": 1586,
    "preview": "import { Check } from \"lucide-react\"\nimport { PlanSelector } from \"./plan-selector\"\nimport { getAccount } from \"~/lib/se"
  },
  {
    "path": "src/app/(main)/subscription/plan-selector.tsx",
    "chars": 1774,
    "preview": "\"use client\"\nimport { cn } from \"~/lib/utils\"\nimport { useState } from \"react\"\nimport { Button } from \"~/components/ui/b"
  },
  {
    "path": "src/app/(main)/subscription/result/page.tsx",
    "chars": 1586,
    "preview": "// import type { Stripe } from \"stripe\"\nimport { stripe } from \"~/lib/stripe\"\nimport { ScrollArea } from \"~/components/u"
  },
  {
    "path": "src/app/(main)/tv-shows/page.tsx",
    "chars": 1128,
    "preview": "import { ShowsCarousel } from \"~/components/show-carousel\"\nimport { getShows } from \"~/lib/client-fetchers\"\nimport { Sho"
  },
  {
    "path": "src/app/(profile)/loading.tsx",
    "chars": 242,
    "preview": "import { Skeleton } from \"~/components/ui/skeleton\"\n\nexport default function Loading() {\n  return (\n    <main className="
  },
  {
    "path": "src/app/(profile)/manage-profile/[...slug]/page.tsx",
    "chars": 2886,
    "preview": "\"use client\"\nimport { Button } from \"~/components/ui/button\"\nimport { useState } from \"react\"\nimport { useDebouncedCallb"
  },
  {
    "path": "src/app/(profile)/manage-profile/add/page.tsx",
    "chars": 1932,
    "preview": "\"use client\"\nimport { Button } from \"~/components/ui/button\"\nimport { Input } from \"~/components/ui/input\"\nimport { useS"
  },
  {
    "path": "src/app/(profile)/manage-profile/page.tsx",
    "chars": 1923,
    "preview": "import { Button } from \"~/components/ui/button\"\nimport { PlusCircle, ArrowLeft, Pencil } from \"lucide-react\"\nimport Link"
  },
  {
    "path": "src/app/(profile)/switch-profile/page.tsx",
    "chars": 973,
    "preview": "import { Button } from \"~/components/ui/button\"\nimport { ArrowLeft } from \"lucide-react\"\nimport Link from \"next/link\"\nim"
  },
  {
    "path": "src/app/(profile)/switch-profile/profile-switcher.tsx",
    "chars": 1029,
    "preview": "\"use client\"\nimport type { Profile } from \"~/lib/types\"\nimport { useRouter } from \"next/navigation\"\nimport { useToast } "
  },
  {
    "path": "src/app/api/(webhook)/stripe/route.ts",
    "chars": 2004,
    "preview": "import type { Stripe } from \"stripe\"\nimport { stripe } from \"~/lib/stripe\"\nimport { NextResponse } from \"next/server\"\nim"
  },
  {
    "path": "src/app/error.tsx",
    "chars": 784,
    "preview": "\"use client\"\n\nimport { useEffect } from \"react\"\nimport { Button } from \"~/components/ui/button\"\n\nexport default function"
  },
  {
    "path": "src/app/layout.tsx",
    "chars": 1724,
    "preview": "import \"~/lib/globals.css\"\nimport { Inter } from \"next/font/google\"\nimport { cn } from \"~/lib/utils\"\nimport { ThemeProvi"
  },
  {
    "path": "src/app/not-found.tsx",
    "chars": 460,
    "preview": "import Link from \"next/link\"\nimport { Button } from \"~/components/ui/button\"\n\nexport default function NotFound() {\n  ret"
  },
  {
    "path": "src/components/link-button.tsx",
    "chars": 338,
    "preview": "\"use client\"\nimport { useRouter } from \"next/navigation\"\n\nexport function LinkButton({\n  children,\n  href,\n}: {\n  childr"
  },
  {
    "path": "src/components/modal-card.tsx",
    "chars": 2839,
    "preview": "\"use client\"\nimport type { ShowWithVideoAndGenre } from \"~/lib/types\"\nimport {\n  Card,\n  CardContent,\n  CardDescription,"
  },
  {
    "path": "src/components/overlay-scrollbar.tsx",
    "chars": 609,
    "preview": "\"use client\"\nimport { useOverlayScrollbars } from \"overlayscrollbars-react\"\nimport \"overlayscrollbars/overlayscrollbars."
  },
  {
    "path": "src/components/show-bg.tsx",
    "chars": 552,
    "preview": "import { type Show } from \"~/lib/types\"\nimport Image from \"next/image\"\n\nexport function ShowBg({ show }: { show: Show })"
  },
  {
    "path": "src/components/show-carousel.tsx",
    "chars": 3150,
    "preview": "\"use client\"\nimport { useSnapCarousel } from \"react-snap-carousel\"\nimport type { Show } from \"~/lib/types\"\nimport { useR"
  },
  {
    "path": "src/components/show-hero.tsx",
    "chars": 1341,
    "preview": "import { type Show } from \"~/lib/types\"\nimport { Play, Info } from \"lucide-react\"\nimport Link from \"next/link\"\nimport { "
  },
  {
    "path": "src/components/theme-provider.tsx",
    "chars": 292,
    "preview": "\"use client\"\nimport { ThemeProvider as NextThemeProvider } from \"next-themes\"\nimport { type ThemeProviderProps } from \"n"
  },
  {
    "path": "src/components/ui/button.tsx",
    "chars": 1819,
    "preview": "import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class"
  },
  {
    "path": "src/components/ui/card.tsx",
    "chars": 1880,
    "preview": "import * as React from \"react\"\n\nimport { cn } from \"src/lib/utils\"\n\nconst Card = React.forwardRef<\n  HTMLDivElement,\n  R"
  },
  {
    "path": "src/components/ui/dropdown-menu.tsx",
    "chars": 7311,
    "preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\"\nimpo"
  },
  {
    "path": "src/components/ui/input.tsx",
    "chars": 885,
    "preview": "/* eslint-disable @typescript-eslint/no-empty-interface */\nimport * as React from \"react\"\n\nimport { cn } from \"src/lib/u"
  },
  {
    "path": "src/components/ui/label.tsx",
    "chars": 726,
    "preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as LabelPrimitive from \"@radix-ui/react-label\"\nimport { cva, type "
  },
  {
    "path": "src/components/ui/scroll-area.tsx",
    "chars": 1647,
    "preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as ScrollAreaPrimitive from \"@radix-ui/react-scroll-area\"\n\nimport "
  },
  {
    "path": "src/components/ui/separator.tsx",
    "chars": 770,
    "preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as SeparatorPrimitive from \"@radix-ui/react-separator\"\n\nimport { c"
  },
  {
    "path": "src/components/ui/skeleton.tsx",
    "chars": 263,
    "preview": "import { cn } from \"src/lib/utils\"\n\nfunction Skeleton({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>)"
  },
  {
    "path": "src/components/ui/toast.tsx",
    "chars": 4829,
    "preview": "import * as React from \"react\"\nimport * as ToastPrimitives from \"@radix-ui/react-toast\"\nimport { cva, type VariantProps "
  },
  {
    "path": "src/components/ui/toaster.tsx",
    "chars": 794,
    "preview": "\"use client\"\n\nimport {\n  Toast,\n  ToastClose,\n  ToastDescription,\n  ToastProvider,\n  ToastTitle,\n  ToastViewport,\n} from"
  },
  {
    "path": "src/components/ui/use-toast.ts",
    "chars": 3927,
    "preview": "// Inspired by react-hot-toast library\nimport * as React from \"react\"\n\nimport type {\n  ToastActionElement,\n  ToastProps,"
  },
  {
    "path": "src/db/client.ts",
    "chars": 294,
    "preview": "import { env } from \"~/env.mjs\"\nimport * as schema from \"./schema\"\nimport { neon, neonConfig } from \"@neondatabase/serve"
  },
  {
    "path": "src/db/migrate.ts",
    "chars": 384,
    "preview": "import { drizzle, type PostgresJsDatabase } from \"drizzle-orm/postgres-js\"\nimport { migrate } from \"drizzle-orm/postgres"
  },
  {
    "path": "src/db/schema.ts",
    "chars": 2503,
    "preview": "import { relations } from \"drizzle-orm\"\nimport {\n  pgTable,\n  varchar,\n  integer,\n  timestamp,\n  pgEnum,\n  primaryKey,\n "
  },
  {
    "path": "src/env.mjs",
    "chars": 2011,
    "preview": "import { createEnv } from \"@t3-oss/env-nextjs\"\nimport { z } from \"zod\"\n\nexport const env = createEnv({\n  /**\n   * Specif"
  },
  {
    "path": "src/lib/client-fetchers.ts",
    "chars": 2166,
    "preview": "import { env } from \"~/env.mjs\"\nimport type { Show, MediaType } from \"./types\"\nimport { ERR } from \"./utils\"\n\nexport asy"
  },
  {
    "path": "src/lib/configs.ts",
    "chars": 784,
    "preview": "export const PLANS = [\n  {\n    id: \"0\",\n    name: \"free\",\n    price: 0,\n    description: \"Free Video Stream\",\n  },\n  {\n "
  },
  {
    "path": "src/lib/globals.css",
    "chars": 1406,
    "preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer base {\n  :root {\n    --background: 0 0% 100%;\n    --f"
  },
  {
    "path": "src/lib/server-fetchers.ts",
    "chars": 3444,
    "preview": "import { env } from \"~/env.mjs\"\nimport type { Show, MyShow, ShowWithVideoAndGenre } from \"~/lib/types\"\nimport { ERR } fr"
  },
  {
    "path": "src/lib/stripe.ts",
    "chars": 170,
    "preview": "import Stripe from \"stripe\"\nimport { env } from \"~/env.mjs\"\n\nexport const stripe = new Stripe(env.STRIPE_SECRET_KEY, {\n "
  },
  {
    "path": "src/lib/types.ts",
    "chars": 1688,
    "preview": "import { type myShows, type profiles, mediaTypeEnum } from \"~/db/schema\"\nimport type { PLANS } from \"~/lib/configs\"\n\nexp"
  },
  {
    "path": "src/lib/utils.ts",
    "chars": 639,
    "preview": "import { type ClassValue, clsx } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\nimport type { Show } from \"./types"
  },
  {
    "path": "src/middleware.ts",
    "chars": 314,
    "preview": "import { authMiddleware } from \"@clerk/nextjs\"\nexport default authMiddleware({\n  publicRoutes: [\n    \"/\",\n    \"/api/(.*)"
  },
  {
    "path": "tailwind.config.ts",
    "chars": 2368,
    "preview": "/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n  darkMode: [\"class\"],\n  content: [\n    \"./pages/**/*.{ts"
  },
  {
    "path": "tsconfig.json",
    "chars": 819,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"es2017\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    "
  }
]

About this extraction

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

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

Copied to clipboard!