main 0a0ad44894ab cached
82 files
108.0 KB
28.9k tokens
94 symbols
1 requests
Download .txt
Repository: jason-liu22/netflix-clone-react-typescript
Branch: main
Commit: 0a0ad44894ab
Files: 82
Total size: 108.0 KB

Directory structure:
gitextract_evgpsfy5/

├── .dockerignore
├── .firebaserc
├── .github/
│   └── workflows/
│       ├── firebase-hosting-merge.yml
│       └── firebase-hosting-pull-request.yml
├── .gitignore
├── Dockerfile
├── README.md
├── firebase.json
├── index.html
├── package.json
├── public/
│   ├── index.html
│   ├── manifest.json
│   └── robots.txt
├── src/
│   ├── CustomClassNameSetup.ts
│   ├── components/
│   │   ├── AgeLimitChip.tsx
│   │   ├── DetailModal.tsx
│   │   ├── GenreBreadcrumbs.tsx
│   │   ├── GridPage.tsx
│   │   ├── GridWithInfiniteScroll.tsx
│   │   ├── HeroSection.tsx
│   │   ├── Logo.tsx
│   │   ├── MainLoadingScreen.tsx
│   │   ├── MaturityRate.tsx
│   │   ├── MaxLineTypography.tsx
│   │   ├── MoreInfoButton.tsx
│   │   ├── NetflixIconButton.tsx
│   │   ├── NetflixNavigationLink.tsx
│   │   ├── PlayButton.tsx
│   │   ├── QualityChip.tsx
│   │   ├── SearchBox.tsx
│   │   ├── SimilarVideoCard.tsx
│   │   ├── VideoCardPortal.tsx
│   │   ├── VideoItemWithHover.tsx
│   │   ├── VideoItemWithHoverPure.tsx
│   │   ├── VideoPortalContainer.tsx
│   │   ├── VideoSlider.tsx
│   │   ├── animate/
│   │   │   ├── MotionContainer.tsx
│   │   │   └── variants/
│   │   │       ├── Wrap.ts
│   │   │       ├── fade/
│   │   │       │   ├── FadeIn.ts
│   │   │       │   └── FadeOut.ts
│   │   │       └── zoom/
│   │   │           └── ZoomIn.ts
│   │   ├── layouts/
│   │   │   ├── Footer.tsx
│   │   │   ├── MainHeader.tsx
│   │   │   └── index.ts
│   │   ├── slick-slider/
│   │   │   ├── CustomNavigation.tsx
│   │   │   └── SlickSlider.tsx
│   │   └── watch/
│   │       ├── PlayerControlButton.tsx
│   │       ├── PlayerSeekbar.tsx
│   │       ├── VideoJSPlayer.tsx
│   │       └── VolumeControllers.tsx
│   ├── constant/
│   │   └── index.ts
│   ├── hoc/
│   │   └── withPagination.tsx
│   ├── hooks/
│   │   ├── redux.ts
│   │   ├── useIntersectionObserver.ts
│   │   ├── useOffSetTop.ts
│   │   └── useWindowSize.ts
│   ├── layouts/
│   │   └── MainLayout.tsx
│   ├── lib/
│   │   └── createSafeContext.ts
│   ├── main.tsx
│   ├── pages/
│   │   ├── GenreExplore.tsx
│   │   ├── HomePage.tsx
│   │   └── WatchPage.tsx
│   ├── providers/
│   │   ├── DetailModalProvider.tsx
│   │   └── PortalProvider.tsx
│   ├── routes/
│   │   └── index.tsx
│   ├── store/
│   │   ├── index.ts
│   │   └── slices/
│   │       ├── apiSlice.ts
│   │       ├── configuration.ts
│   │       ├── discover.ts
│   │       └── genre.ts
│   ├── theme/
│   │   └── palette.ts
│   ├── types/
│   │   ├── Common.ts
│   │   ├── Genre.ts
│   │   └── Movie.ts
│   ├── utils/
│   │   ├── common.ts
│   │   └── index.ts
│   ├── videojs-youtube.d.ts
│   └── vite-env.d.ts
├── tsconfig.json
├── tsconfig.node.json
├── vercel.json
└── vite.config.ts

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

================================================
FILE: .dockerignore
================================================
.*
build
node_modules

================================================
FILE: .firebaserc
================================================
{
  "projects": {
    "default": "websites-f0426"
  }
}


================================================
FILE: .github/workflows/firebase-hosting-merge.yml
================================================
# This file was auto-generated by the Firebase CLI
# https://github.com/firebase/firebase-tools

name: Deploy to Firebase Hosting on merge
'on':
  push:
    branches:
      - main
jobs:
  build_and_deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - run: npm ci && npm run build
        env:
          VITE_APP_API_ENDPOINT_URL: '${{ secrets.VITE_APP_API_ENDPOINT_URL }}'
          VITE_APP_TMDB_V3_API_KEY: '${{ secrets.VITE_APP_TMDB_V3_API_KEY }}'
      - uses: FirebaseExtended/action-hosting-deploy@v0
        with:
          repoToken: '${{ secrets.GITHUB_TOKEN }}'
          firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_WEBSITES_F0426 }}'
          channelId: live
          projectId: websites-f0426


================================================
FILE: .github/workflows/firebase-hosting-pull-request.yml
================================================
# This file was auto-generated by the Firebase CLI
# https://github.com/firebase/firebase-tools

name: Deploy to Firebase Hosting on PR
'on': pull_request
jobs:
  build_and_preview:
    if: '${{ github.event.pull_request.head.repo.full_name == github.repository }}'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - run: npm ci && npm run build
      - uses: FirebaseExtended/action-hosting-deploy@v0
        with:
          repoToken: '${{ secrets.GITHUB_TOKEN }}'
          firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_WEBSITES_F0426 }}'
          projectId: websites-f0426


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

# dependencies
/node_modules
/.pnp
.pnp.js

# env
.env

# testing
/coverage

# production
/build
/dist

# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn.lock
yarn-debug.log*
yarn-error.log*


================================================
FILE: Dockerfile
================================================
FROM node:16.17.0-alpine as builder
WORKDIR /app
COPY ./package.json .
COPY ./yarn.lock .
RUN yarn install
COPY . .
ARG TMDB_V3_API_KEY
ENV VITE_APP_TMDB_V3_API_KEY=${TMDB_V3_API_KEY}
ENV VITE_APP_API_ENDPOINT_URL="https://api.themoviedb.org/3"
RUN yarn build

FROM nginx:stable-alpine
WORKDIR /usr/share/nginx/html
RUN rm -rf ./*
COPY --from=builder /app/dist .
EXPOSE 80
ENTRYPOINT ["nginx", "-g", "daemon off;"]

================================================
FILE: README.md
================================================
<div align="center">
  <a href="http://netflix-clone-with-tmdb-using-react-mui.vercel.app/">
    <img src="./public/assets/netflix-logo.png" alt="Logo" width="100" height="32">
  </a>

  <h3 align="center">Netflix Clone</h3>

  <p align="center">
    <a href="https://netflix-clone-react-typescript.vercel.app/">View Demo</a>
    ·
    <a href="https://github.com/crazy-man22/netflix-clone-react-typescript/issues">Report Bug</a>
    ·
    <a href="https://github.com/crazy-man22/netflix-clone-react-typescript/issues">Request Feature</a>
  </p>
</div>

<details>
  <summary>Table of Contents</summary>
  <ol>
    <li>
      <a href="#prerequests">Prerequests</a>
    </li>
    <li>
      <a href="#which-features-this-project-deals-with">Which features this project deals with</a>
    </li>
    <li><a href="#third-party-libraries-used-except-for-react-and-rtk">Third Party libraries used except for React and RTK</a></li>
    <li>
      <a href="#contact">Contact</a>
    </li>
  </ol>
</details>

<br />

<div align="center">
  <img src="./public/assets/home-page.png" alt="Logo" width="100%" height="100%">
  <p align="center">Home Page</p>
  <img src="./public/assets/mini-portal.png" alt="Logo" width="100%" height="100%">
  <p align="center">Mini Portal</p>
  <img src="./public/assets/detail-modal.png" alt="Logo" width="100%" height="100%">
  <p align="center">Detail Modal</p>
  <img src="./public/assets/grid-genre.png" alt="Logo" width="100%" height="100%">
  <p align="center">Grid Genre Page</p>
  <img src="./public/assets/watch.png" alt="Logo" width="100%" height="100%">
  <p align="center">Watch Page with customer contol bar</p>
</div>

## Prerequests

- Create an account if you don't have on [TMDB](https://www.themoviedb.org/).
  Because I use its free API to consume movie/tv data.
- And then follow the [documentation](https://developers.themoviedb.org/3/getting-started/introduction) to create API Key
- Finally, if you use v3 of TMDB API, create a file named `.env`, and copy and paste the content of `.env.example`.
  And then paste the API Key you just created.

## Which features this project deal with

- How to create and use [Custom Hooks](https://reactjs.org/docs/hooks-custom.html)
- How to use [Context](https://reactjs.org/docs/context.html) and its provider
- How to use lazy and Suspense for [Code-Splitting](https://reactjs.org/docs/code-splitting.html)
- How to use a new [lazy](https://reactrouter.com/en/main/route/lazy) feature of react-router to reduce bundle size.
- How to use data [loader](https://reactrouter.com/en/main/route/loader) of react-router, and how to use redux dispatch in the loader to fetch data before rendering component.
- How to use [Portal](https://reactjs.org/docs/portals.html)
- How to use [Fowarding Refs](https://reactjs.org/docs/forwarding-refs.html) to make components reusuable
- How to create and use [HOC](https://reactjs.org/docs/higher-order-components.html)
- How to customize default theme of [MUI](https://mui.com/)
- How to use [RTK](https://redux-toolkit.js.org/introduction/getting-started)
- How to use [RTK Query](https://redux-toolkit.js.org/rtk-query/overview)
- How to customize default classname of [MUI](https://mui.com/material-ui/experimental-api/classname-generator)
- Infinite Scrolling(using [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API))
- How to make awesome carousel using [slick-carousel](https://react-slick.neostack.com)

## Third Party libraries used except for React and RTK

- [react-router-dom@v6.9](https://reactrouter.com/en/main)
- [MUI(Material UI)](https://mui.com/)
- [framer-motion](https://www.framer.com/docs/)
- [video.js](https://videojs.com)
- [react-slick](https://react-slick.neostack.com/)

## Install with Docker

```sh
docker build --build-arg TMDB_V3_API_KEY=your_api_key_here -t netflix-clone .

docker run --name netflix-clone-website --rm -d -p 80:80 netflix-clone
```

## Todo

- Make the animation of video card portal more similar to Netflix.
- Improve performance. I am using `context` and `provider` but all components subscribed to the context's value are re-rendered. These re-renders happen even if the part of the value is not used in render of the component. there are [several ways](https://blog.axlight.com/posts/4-options-to-prevent-extra-rerenders-with-react-context/) to prevent the re-renders from these behaviours. In addition to them, there may be several performance issues.
- Replace bundler([Vite](https://vitejs.dev/guide)) with [Turbopack](https://turbo.build/pack/docs/why-turbopack). Turbopack is introduced in Next.js conf recently. It's very fast but it's nor ready to use right now. it just support Next.js, and they plan to support all others as soon as possible. so if it's ready to use, replace [Vite](https://vitejs.dev/guide) with [Turbopack](https://turbo.build/pack/docs/why-turbopack).
- Add accessibilities for better UX.
- Add Tests.


================================================
FILE: firebase.json
================================================
{
  "hosting": {
    "public": "dist",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "rewrites": [
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
  }
}


================================================
FILE: index.html
================================================
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="apple-touch-icon" href="/logo192.png" />
    <link rel="manifest" href="/manifest.json" />
    <link
      rel="stylesheet"
      href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
    />
    <link
      rel="stylesheet"
      href="https://fonts.googleapis.com/icon?family=Material+Icons"
    />
    <title>Netflix</title>
  </head>
  <body style="margin: 0">
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>


================================================
FILE: package.json
================================================
{
  "name": "netflix-clone-using-react-typescript-mui",
  "version": "0.1.1",
  "type": "module",
  "private": true,
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "@emotion/react": "^11.10.0",
    "@emotion/styled": "^11.10.0",
    "@mui/icons-material": "^5.8.4",
    "@mui/material": "^5.10.0",
    "@reduxjs/toolkit": "^1.8.3",
    "framer-motion": "^7.1.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-redux": "^8.0.2",
    "react-router-dom": "^6.9.0",
    "react-slick": "^0.29.0",
    "slick-carousel": "^1.8.1",
    "video.js": "^8.3.0",
    "videojs-youtube": "^3.0.1",
    "vite-tsconfig-paths": "^3.5.2"
  },
  "devDependencies": {
    "@types/react": "^18.0.24",
    "@types/react-dom": "^18.0.8",
    "@types/react-slick": "^0.23.10",
    "@vitejs/plugin-react": "^2.2.0",
    "typescript": "^4.6.4",
    "vite": "^3.2.1"
  }
}


================================================
FILE: public/index.html
================================================
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
    <!--
      manifest.json provides metadata used when your web app is installed on a
      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
    -->
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <link
      rel="stylesheet"
      href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
    />
    <link
      rel="stylesheet"
      href="https://fonts.googleapis.com/icon?family=Material+Icons"
    />
    <!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.

      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    -->
    <title>Netflix</title>
  </head>
  <body style="margin: 0">
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.

      You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.

      To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    -->
  </body>
</html>


================================================
FILE: public/manifest.json
================================================
{
  "short_name": "React App",
  "name": "Create React App Sample",
  "icons": [
    {
      "src": "favicon.ico",
      "sizes": "64x64 32x32 24x24 16x16",
      "type": "image/x-icon"
    },
    {
      "src": "logo192.png",
      "type": "image/png",
      "sizes": "192x192"
    },
    {
      "src": "logo512.png",
      "type": "image/png",
      "sizes": "512x512"
    }
  ],
  "start_url": ".",
  "display": "standalone",
  "theme_color": "#000000",
  "background_color": "#ffffff"
}


================================================
FILE: public/robots.txt
================================================
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:


================================================
FILE: src/CustomClassNameSetup.ts
================================================
import { unstable_ClassNameGenerator as ClassNameGenerator } from "@mui/material/className";

ClassNameGenerator.configure((componentName) => {
  let newComponentName = componentName;
  // you can replace Mui, default prefix of every component with new one you want
  newComponentName = newComponentName.replace("Mui", "Netflix");
  // you can replace default classname of component with new one you want
  newComponentName = newComponentName.replace("Button", "Btn");

  return newComponentName;
});


================================================
FILE: src/components/AgeLimitChip.tsx
================================================
import Chip, { ChipProps } from "@mui/material/Chip";
export default function AgeLimitChip({ sx, ...others }: ChipProps) {
  return (
    <Chip
      {...others}
      sx={{
        borderRadius: 0,
        p: 0.5,
        fontSize: 12,
        height: "100%",
        "& > span": { p: 0 },
        ...sx,
      }}
      variant="outlined"
    />
  );
}


================================================
FILE: src/components/DetailModal.tsx
================================================
import { forwardRef, useCallback, useRef, useState } from "react";
import Box from "@mui/material/Box";
import Grid from "@mui/material/Grid";
import Container from "@mui/material/Container";
import Stack from "@mui/material/Stack";
import IconButton from "@mui/material/IconButton";
import Typography from "@mui/material/Typography";
import Dialog from "@mui/material/Dialog";
import DialogContent from "@mui/material/DialogContent";
import Slide from "@mui/material/Slide";
import { TransitionProps } from "@mui/material/transitions";
import CloseIcon from "@mui/icons-material/Close";
import AddIcon from "@mui/icons-material/Add";
import ThumbUpOffAltIcon from "@mui/icons-material/ThumbUpOffAlt";
import VolumeUpIcon from "@mui/icons-material/VolumeUp";
import VolumeOffIcon from "@mui/icons-material/VolumeOff";
import Player from "video.js/dist/types/player";

import MaxLineTypography from "./MaxLineTypography";
import PlayButton from "./PlayButton";
import NetflixIconButton from "./NetflixIconButton";
import AgeLimitChip from "./AgeLimitChip";
import QualityChip from "./QualityChip";
import { formatMinuteToReadable, getRandomNumber } from "src/utils/common";
import SimilarVideoCard from "./SimilarVideoCard";
import { useDetailModal } from "src/providers/DetailModalProvider";
import { useGetSimilarVideosQuery } from "src/store/slices/discover";
import { MEDIA_TYPE } from "src/types/Common";
import VideoJSPlayer from "./watch/VideoJSPlayer";

const Transition = forwardRef(function Transition(
  props: TransitionProps & {
    children: React.ReactElement<any, any>;
  },
  ref: React.Ref<unknown>
) {
  return <Slide direction="up" ref={ref} {...props} />;
});

export default function DetailModal() {
  const { detail, setDetailType } = useDetailModal();
  const { data: similarVideos } = useGetSimilarVideosQuery(
    { mediaType: detail.mediaType ?? MEDIA_TYPE.Movie, id: detail.id ?? 0 },
    { skip: !detail.id }
  );
  const playerRef = useRef<Player | null>(null);
  const [muted, setMuted] = useState(true);

  const handleReady = useCallback((player: Player) => {
    playerRef.current = player;
    setMuted(player.muted());
  }, []);

  const handleMute = useCallback((status: boolean) => {
    if (playerRef.current) {
      playerRef.current.muted(!status);
      setMuted(!status);
    }
  }, []);

  if (detail.mediaDetail) {
    return (
      <Dialog
        fullWidth
        scroll="body"
        maxWidth="md"
        open={!!detail.mediaDetail}
        id="detail_dialog"
        TransitionComponent={Transition}
      >
        <DialogContent sx={{ p: 0, bgcolor: "#181818" }}>
          <Box
            sx={{
              top: 0,
              left: 0,
              right: 0,
              position: "relative",
              mb: 3,
            }}
          >
            <Box
              sx={{
                width: "100%",
                position: "relative",
                height: "calc(9 / 16 * 100%)",
              }}
            >
              <VideoJSPlayer
                options={{
                  loop: true,
                  autoplay: true,
                  controls: false,
                  responsive: true,
                  fluid: true,
                  techOrder: ["youtube"],
                  sources: [
                    {
                      type: "video/youtube",
                      src: `https://www.youtube.com/watch?v=${
                        detail.mediaDetail?.videos.results[0]?.key ||
                        "L3oOldViIgY"
                      }`,
                    },
                  ],
                }}
                onReady={handleReady}
              />

              <Box
                sx={{
                  background: `linear-gradient(77deg,rgba(0,0,0,.6),transparent 85%)`,
                  top: 0,
                  left: 0,
                  bottom: 0,
                  right: "26.09%",
                  opacity: 1,
                  position: "absolute",
                  transition: "opacity .5s",
                }}
              />
              <Box
                sx={{
                  backgroundColor: "transparent",
                  backgroundImage:
                    "linear-gradient(180deg,hsla(0,0%,8%,0) 0,hsla(0,0%,8%,.15) 15%,hsla(0,0%,8%,.35) 29%,hsla(0,0%,8%,.58) 44%,#141414 68%,#141414)",
                  backgroundRepeat: "repeat-x",
                  backgroundPosition: "0px top",
                  backgroundSize: "100% 100%",
                  bottom: 0,
                  position: "absolute",
                  height: "14.7vw",
                  opacity: 1,
                  top: "auto",
                  width: "100%",
                }}
              />
              <IconButton
                onClick={() => {
                  setDetailType({ mediaType: undefined, id: undefined });
                }}
                sx={{
                  top: 15,
                  right: 15,
                  position: "absolute",
                  bgcolor: "#181818",
                  width: { xs: 22, sm: 40 },
                  height: { xs: 22, sm: 40 },
                  "&:hover": {
                    bgcolor: "primary.main",
                  },
                }}
              >
                <CloseIcon
                  sx={{ color: "white", fontSize: { xs: 14, sm: 22 } }}
                />
              </IconButton>
              <Box
                sx={{
                  position: "absolute",
                  left: 0,
                  right: 0,
                  bottom: 16,
                  px: { xs: 2, sm: 3, md: 5 },
                }}
              >
                <MaxLineTypography variant="h4" maxLine={1} sx={{ mb: 2 }}>
                  {detail.mediaDetail?.title}
                </MaxLineTypography>
                <Stack direction="row" spacing={2} sx={{ mb: 3 }}>
                  <PlayButton sx={{ color: "black", py: 0 }} />
                  <NetflixIconButton>
                    <AddIcon />
                  </NetflixIconButton>
                  <NetflixIconButton>
                    <ThumbUpOffAltIcon />
                  </NetflixIconButton>
                  <Box flexGrow={1} />
                  <NetflixIconButton
                    size="large"
                    onClick={() => handleMute(muted)}
                    sx={{ zIndex: 2 }}
                  >
                    {!muted ? <VolumeUpIcon /> : <VolumeOffIcon />}
                  </NetflixIconButton>
                </Stack>

                <Container
                  sx={{
                    p: "0px !important",
                  }}
                >
                  <Grid container spacing={5} alignItems="center">
                    <Grid item xs={12} sm={6} md={8}>
                      <Stack direction="row" spacing={1} alignItems="center">
                        <Typography
                          variant="subtitle1"
                          sx={{ color: "success.main" }}
                        >{`${getRandomNumber(100)}% Match`}</Typography>
                        <Typography variant="body2">
                          {detail.mediaDetail?.release_date.substring(0, 4)}
                        </Typography>
                        <AgeLimitChip label={`${getRandomNumber(20)}+`} />
                        <Typography variant="subtitle2">{`${formatMinuteToReadable(
                          getRandomNumber(180)
                        )}`}</Typography>
                        <QualityChip label="HD" />
                      </Stack>

                      <MaxLineTypography
                        maxLine={3}
                        variant="body1"
                        sx={{ mt: 2 }}
                      >
                        {detail.mediaDetail?.overview}
                      </MaxLineTypography>
                    </Grid>
                    <Grid item xs={12} sm={6} md={4}>
                      <Typography variant="body2" sx={{ my: 1 }}>
                        {`Genres : ${detail.mediaDetail?.genres
                          .map((g) => g.name)
                          .join(", ")}`}
                      </Typography>
                      <Typography variant="body2" sx={{ my: 1 }}>
                        {`Available in : ${detail.mediaDetail?.spoken_languages
                          .map((l) => l.name)
                          .join(", ")}`}
                      </Typography>
                    </Grid>
                  </Grid>
                </Container>
              </Box>
            </Box>
            {similarVideos && similarVideos.results.length > 0 && (
              <Container
                sx={{
                  py: 2,
                  px: { xs: 2, sm: 3, md: 5 },
                }}
              >
                <Typography variant="h6" sx={{ mb: 2 }}>
                  More Like This
                </Typography>
                <Grid container spacing={2}>
                  {similarVideos.results.map((sm) => (
                    <Grid item xs={6} sm={4} key={sm.id}>
                      <SimilarVideoCard video={sm} />
                    </Grid>
                  ))}
                </Grid>
              </Container>
            )}
          </Box>
        </DialogContent>
      </Dialog>
    );
  }

  return null;
}


================================================
FILE: src/components/GenreBreadcrumbs.tsx
================================================
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import Breadcrumbs, { BreadcrumbsProps } from "@mui/material/Breadcrumbs";

const Separator = (
  <Box
    component="span"
    sx={{
      width: 4,
      height: 4,
      borderRadius: "50%",
      bgcolor: "text.disabled",
    }}
  />
);

interface GenreBreadcrumbsProps extends BreadcrumbsProps {
  genres: string[];
}

export default function GenreBreadcrumbs({
  genres,
  ...others
}: GenreBreadcrumbsProps) {
  return (
    <Breadcrumbs separator={Separator} {...others}>
      {genres.map((genre, idx) => (
        <Typography key={idx} sx={{ color: "text.primary" }}>
          {genre}
        </Typography>
      ))}
    </Breadcrumbs>
  );
}


================================================
FILE: src/components/GridPage.tsx
================================================
import withPagination from "src/hoc/withPagination";
import { MEDIA_TYPE } from "src/types/Common";
import { CustomGenre, Genre } from "src/types/Genre";
import GridWithInfiniteScroll from "./GridWithInfiniteScroll";

interface GridPageProps {
  genre: Genre | CustomGenre;
  mediaType: MEDIA_TYPE;
}
export default function GridPage({ genre, mediaType }: GridPageProps) {
  const Component = withPagination(
    GridWithInfiniteScroll,
    mediaType,
    genre
  );
  return <Component />;
}


================================================
FILE: src/components/GridWithInfiniteScroll.tsx
================================================
import { useRef, useEffect } from "react";
import Grid from "@mui/material/Grid";
import Box from "@mui/material/Box";
import Container from "@mui/material/Container";
import Typography from "@mui/material/Typography";
import VideoItemWithHover from "./VideoItemWithHover";
import { CustomGenre, Genre } from "src/types/Genre";
import { PaginatedMovieResult } from "src/types/Common";
import useIntersectionObserver from "src/hooks/useIntersectionObserver";

interface GridWithInfiniteScrollProps {
  genre: Genre | CustomGenre;
  data: PaginatedMovieResult;
  handleNext: (page: number) => void;
}
export default function GridWithInfiniteScroll({
  genre,
  data,
  handleNext,
}: GridWithInfiniteScrollProps) {
  const intersectionRef = useRef<HTMLDivElement>(null);
  const intersection = useIntersectionObserver(intersectionRef);

  useEffect(() => {
    if (
      intersection &&
      intersection.intersectionRatio === 1 &&
      data.page < data.total_pages
    ) {
      handleNext(data.page + 1);
    }
  }, [intersection]);

  return (
    <>
      <Container
        maxWidth={false}
        sx={{
          px: { xs: "30px", sm: "60px" },
          pb: 4,
          pt: "150px",
          bgcolor: "inherit",
        }}
      >
        <Typography
          variant="h5"
          sx={{ color: "text.primary", mb: 2 }}
        >{`${genre.name} Movies`}</Typography>
        <Grid container spacing={2}>
          {data.results
            .filter((v) => !!v.backdrop_path)
            .map((video, idx) => (
              <Grid
                key={`${video.id}_${idx}`}
                item
                xs={6}
                sm={3}
                md={2}
                sx={{ zIndex: 1 }}
              >
                <VideoItemWithHover video={video} />
              </Grid>
            ))}
        </Grid>
      </Container>
      <Box sx={{ display: "hidden" }} ref={intersectionRef} />
    </>
  );
}


================================================
FILE: src/components/HeroSection.tsx
================================================
import { useEffect, useState, useMemo, useCallback, useRef } from "react";
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import VolumeUpIcon from "@mui/icons-material/VolumeUp";
import VolumeOffIcon from "@mui/icons-material/VolumeOff";
import Player from "video.js/dist/types/player";

import { getRandomNumber } from "src/utils/common";
import MaxLineTypography from "./MaxLineTypography";
import PlayButton from "./PlayButton";
import MoreInfoButton from "./MoreInfoButton";
import NetflixIconButton from "./NetflixIconButton";
import MaturityRate from "./MaturityRate";
import useOffSetTop from "src/hooks/useOffSetTop";
import { useDetailModal } from "src/providers/DetailModalProvider";
import { MEDIA_TYPE } from "src/types/Common";
import {
  useGetVideosByMediaTypeAndCustomGenreQuery,
  useLazyGetAppendedVideosQuery,
} from "src/store/slices/discover";
import { Movie } from "src/types/Movie";
import VideoJSPlayer from "./watch/VideoJSPlayer";

interface TopTrailerProps {
  mediaType: MEDIA_TYPE;
}

export default function TopTrailer({ mediaType }: TopTrailerProps) {
  const { data } = useGetVideosByMediaTypeAndCustomGenreQuery({
    mediaType,
    apiString: "popular",
    page: 1,
  });
  const [getVideoDetail, { data: detail }] = useLazyGetAppendedVideosQuery();
  const [video, setVideo] = useState<Movie | null>(null);
  const [muted, setMuted] = useState(true);
  const playerRef = useRef<Player | null>(null);
  const isOffset = useOffSetTop(window.innerWidth * 0.5625);
  const { setDetailType } = useDetailModal();
  const maturityRate = useMemo(() => {
    return getRandomNumber(20);
  }, []);

  const handleReady = useCallback((player: Player) => {
    playerRef.current = player;
  }, []);

  useEffect(() => {
    if (playerRef.current) {
      if (isOffset) {
        playerRef.current.pause();
      } else {
        if (playerRef.current.paused()) {
          playerRef.current.play();
        }
      }
    }
  }, [isOffset]);

  useEffect(() => {
    if (data && data.results) {
      const videos = data.results.filter((item) => !!item.backdrop_path);
      setVideo(videos[getRandomNumber(videos.length)]);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [data]);

  useEffect(() => {
    if (video) {
      getVideoDetail({ mediaType, id: video.id });
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [video]);

  const handleMute = useCallback((status: boolean) => {
    if (playerRef.current) {
      playerRef.current.muted(!status);
      setMuted(!status);
    }
  }, []);

  return (
    <Box sx={{ position: "relative", zIndex: 1 }}>
      <Box
        sx={{
          mb: 3,
          pb: "40%",
          top: 0,
          left: 0,
          right: 0,
          position: "relative",
        }}
      >
        <Box
          sx={{
            width: "100%",
            height: "56.25vw",
            position: "absolute",
          }}
        >
          {video && (
            <>
              <Box
                sx={{
                  top: 0,
                  left: 0,
                  right: 0,
                  bottom: 0,
                  position: "absolute",
                }}
              >
                {detail && (
                  <VideoJSPlayer
                    options={{
                      loop: true,
                      muted: true,
                      autoplay: true,
                      controls: false,
                      responsive: true,
                      fluid: true,
                      techOrder: ["youtube"],
                      sources: [
                        {
                          type: "video/youtube",
                          src: `https://www.youtube.com/watch?v=${
                            detail.videos.results[0]?.key || "L3oOldViIgY"
                          }`,
                        },
                      ],
                    }}
                    onReady={handleReady}
                  />
                )}
                <Box
                  sx={{
                    background: `linear-gradient(77deg,rgba(0,0,0,.6),transparent 85%)`,
                    top: 0,
                    left: 0,
                    bottom: 0,
                    right: "26.09%",
                    opacity: 1,
                    position: "absolute",
                    transition: "opacity .5s",
                  }}
                />
                <Box
                  sx={{
                    backgroundColor: "transparent",
                    backgroundImage:
                      "linear-gradient(180deg,hsla(0,0%,8%,0) 0,hsla(0,0%,8%,.15) 15%,hsla(0,0%,8%,.35) 29%,hsla(0,0%,8%,.58) 44%,#141414 68%,#141414)",
                    backgroundRepeat: "repeat-x",
                    backgroundPosition: "0px top",
                    backgroundSize: "100% 100%",
                    bottom: 0,
                    position: "absolute",
                    height: "14.7vw",
                    opacity: 1,
                    top: "auto",
                    width: "100%",
                  }}
                />
                <Stack
                  direction="row"
                  spacing={2}
                  sx={{
                    alignItems: "center",
                    position: "absolute",
                    right: 0,
                    bottom: "35%",
                  }}
                >
                  <NetflixIconButton
                    size="large"
                    onClick={() => handleMute(muted)}
                    sx={{ zIndex: 2 }}
                  >
                    {!muted ? <VolumeUpIcon /> : <VolumeOffIcon />}
                  </NetflixIconButton>
                  <MaturityRate>{`${maturityRate}+`}</MaturityRate>
                </Stack>
              </Box>

              <Box
                sx={{
                  position: "absolute",
                  top: 0,
                  left: 0,
                  right: 0,
                  bottom: 0,
                  width: "100%",
                  height: "100%",
                }}
              >
                <Stack
                  spacing={4}
                  sx={{
                    bottom: "35%",
                    position: "absolute",
                    left: { xs: "4%", md: "60px" },
                    top: 0,
                    width: "36%",
                    zIndex: 10,
                    justifyContent: "flex-end",
                  }}
                >
                  <MaxLineTypography
                    variant="h2"
                    maxLine={1}
                    color="text.primary"
                  >
                    {video.title}
                  </MaxLineTypography>
                  <MaxLineTypography
                    variant="h5"
                    maxLine={3}
                    color="text.primary"
                  >
                    {video.overview}
                  </MaxLineTypography>
                  <Stack direction={{ xs: "column", sm: "row" }} spacing={2}>
                    <PlayButton size="large" />
                    <MoreInfoButton
                      size="large"
                      onClick={() => {
                        setDetailType({ mediaType, id: video.id });
                      }}
                    />
                  </Stack>
                </Stack>
              </Box>
            </>
          )}
        </Box>
      </Box>
    </Box>
  );
}


================================================
FILE: src/components/Logo.tsx
================================================
import Box, { BoxProps } from "@mui/material/Box";
import { Link as RouterLink } from "react-router-dom";
import { MAIN_PATH } from "src/constant";

export default function Logo({ sx }: BoxProps) {
  return (
    <RouterLink to={`/${MAIN_PATH.browse}`}>
      <Box
        component="img"
        alt="Netflix Logo"
        src="/assets/netflix-logo.png"
        width={87}
        height={25}
        sx={{
          ...sx,
        }}
      />
    </RouterLink>
  );
}


================================================
FILE: src/components/MainLoadingScreen.tsx
================================================
import CircularProgress from "@mui/material/CircularProgress";

function MainLoadingScreen() {
  return (
    <div
      style={{
        top: 0,
        left: 0,
        right: 0,
        bottom: 0,
        display: "flex",
        justifyContent: "center",
        alignItems: "center",
        position: "fixed",
        backgroundColor: "#141414",
        opacity: 0.75,
        zIndex: 2,
      }}
    >
      <CircularProgress sx={{ color: "white" }} />
    </div>
  );
}

export default MainLoadingScreen;


================================================
FILE: src/components/MaturityRate.tsx
================================================
import Box from "@mui/material/Box";
import { ReactNode } from "react";

export default function MaturityRate({ children }: { children: ReactNode }) {
  return (
    <Box
      sx={{
        py: 1,
        pl: 1.5,
        pr: 3,
        fontSize: 22,
        display: "flex",
        alignItem: "center",
        color: "text.primary",
        border: "3px #dcdcdc",
        borderLeftStyle: "solid",
        bgcolor: "#33333399",
      }}
    >
      {children}
    </Box>
  );
}


================================================
FILE: src/components/MaxLineTypography.tsx
================================================
import { forwardRef } from "react";
import Typography, { TypographyProps } from "@mui/material/Typography";

const MaxLineTypography = forwardRef<
  HTMLDivElement,
  TypographyProps & { maxLine: number }
>(({ maxLine, children, sx, ...others }, ref) => {
  return (
    <Typography
      ref={ref}
      sx={{
        overflow: "hidden",
        textOverflow: "ellipsis",
        display: "-webkit-box",
        WebkitLineClamp: maxLine,
        WebkitBoxOrient: "vertical",
        ...sx,
      }}
      {...others}
    >
      {children}
    </Typography>
  );
});

export default MaxLineTypography;


================================================
FILE: src/components/MoreInfoButton.tsx
================================================
import Button, { ButtonProps } from "@mui/material/Button";
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";

export default function MoreInfoButton({ sx, ...others }: ButtonProps) {
  return (
    <Button
      variant="contained"
      startIcon={
        <InfoOutlinedIcon
          sx={{
            fontSize: {
              xs: "24px !important",
              sm: "32px !important",
              md: "40px !important",
            },
          }}
        />
      }
      {...others}
      sx={{
        ...sx,
        px: { xs: 1, sm: 2 },
        py: { xs: 0.5, sm: 1 },
        fontSize: { xs: 18, sm: 24, md: 28 },
        lineHeight: 1.5,
        fontWeight: "bold",
        textTransform: "capitalize",
        bgcolor: "#6d6d6eb3",
        whiteSpace: "nowrap",
        "&:hover": { bgcolor: "#6d6d6e66" },
      }}
    >
      More Info
    </Button>
  );
}


================================================
FILE: src/components/NetflixIconButton.tsx
================================================
import { forwardRef } from "react";
import IconButton, { IconButtonProps } from "@mui/material/IconButton";

const NetflixIconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
  ({ children, sx, ...others }, ref) => {
    return (
      <IconButton
        sx={{
          color: "white",
          borderWidth: "2px",
          borderStyle: "solid",
          borderColor: "grey.700",
          "&:hover, &:focus": {
            borderColor: "grey.200",
          },
          ...sx,
        }}
        {...others}
        ref={ref}
      >
        {children}
      </IconButton>
    );
  }
);

export default NetflixIconButton;


================================================
FILE: src/components/NetflixNavigationLink.tsx
================================================
import {
  Link as RouterLink,
  LinkProps as RouterLinkProps,
} from "react-router-dom";
import Link, { LinkProps } from "@mui/material/Link";

export default function NetflixNavigationLink({
  sx,
  children,
  ...others
}: LinkProps & RouterLinkProps) {
  return (
    <Link
      {...others}
      component={RouterLink}
      sx={{ color: "text.primary", textDecoration: "none", ...sx }}
    >
      {children}
    </Link>
  );
}


================================================
FILE: src/components/PlayButton.tsx
================================================
import Button, { ButtonProps } from "@mui/material/Button";
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import { useNavigate } from "react-router-dom";
import { MAIN_PATH } from "src/constant";

export default function PlayButton({ sx, ...others }: ButtonProps) {
  const navigate = useNavigate();
  return (
    <Button
      color="inherit"
      variant="contained"
      startIcon={
        <PlayArrowIcon
          sx={{
            fontSize: {
              xs: "24px !important",
              sm: "32px !important",
              md: "40px !important",
            },
          }}
        />
      }
      {...others}
      sx={{
        px: { xs: 1, sm: 2 },
        py: { xs: 0.5, sm: 1 },
        fontSize: { xs: 18, sm: 24, md: 28 },
        lineHeight: 1.5,
        fontWeight: "bold",
        whiteSpace: "nowrap",
        textTransform: "capitalize",
        ...sx,
      }}
      onClick={() => navigate(`/${MAIN_PATH.watch}`)}
    >
      Play
    </Button>
  );
}


================================================
FILE: src/components/QualityChip.tsx
================================================
import Chip, { ChipProps } from "@mui/material/Chip";
export default function QualityChip({ sx, ...others }: ChipProps) {
  return (
    <Chip
      variant="outlined"
      {...others}
      sx={{
        borderRadius: "4px",
        p: 0.5,
        fontSize: 12,
        height: "100%",
        "& > span": { p: 0 },
        ...sx,
      }}
    />
  );
}


================================================
FILE: src/components/SearchBox.tsx
================================================
import { useState, useRef } from "react";
import { styled } from "@mui/material/styles";
import InputBase from "@mui/material/InputBase";
import SearchIcon from "@mui/icons-material/Search";

const Search = styled("div")(({ theme }) => ({
  position: "relative",
  width: "100%",
  display: "flex",
  alignItems: "center",
}));

const SearchIconWrapper = styled("div")(({ theme }) => ({
  cursor: "pointer",
  padding: theme.spacing(0, 1),
  height: "100%",
  display: "flex",
  alignItems: "center",
  justifyContent: "center",
}));

const StyledInputBase = styled(InputBase)(({ theme }) => ({
  color: "inherit",
  "& .NetflixInputBase-input": {
    width: 0,
    transition: theme.transitions.create("width", {
      duration: theme.transitions.duration.complex,
      easing: theme.transitions.easing.easeIn,
    }),
    "&:focus": {
      width: "auto",
    },
  },
}));

export default function SearchBox() {
  const [isFocused, setIsFocused] = useState(false);
  const searchInputRef = useRef<HTMLInputElement>();

  const handleClickSearchIcon = () => {
    if (!isFocused) {
      searchInputRef.current?.focus();
    }
  };

  return (
    <Search
      sx={
        isFocused ? { border: "1px solid white", backgroundColor: "black" } : {}
      }
    >
      <SearchIconWrapper onClick={handleClickSearchIcon}>
        <SearchIcon />
      </SearchIconWrapper>
      <StyledInputBase
        inputRef={searchInputRef}
        placeholder="Titles, people, genres"
        inputProps={{
          "aria-label": "search",
          onFocus: () => {
            setIsFocused(true);
          },
          onBlur: () => {
            setIsFocused(false);
          },
        }}
      />
    </Search>
  );
}


================================================
FILE: src/components/SimilarVideoCard.tsx
================================================
import Stack from "@mui/material/Stack";
import Card from "@mui/material/Card";
import CardContent from "@mui/material/CardContent";
import Typography from "@mui/material/Typography";
import AddIcon from "@mui/icons-material/Add";
import { Movie } from "src/types/Movie";
import NetflixIconButton from "./NetflixIconButton";
import MaxLineTypography from "./MaxLineTypography";
import { formatMinuteToReadable, getRandomNumber } from "src/utils/common";
import AgeLimitChip from "./AgeLimitChip";
import { useGetConfigurationQuery } from "src/store/slices/configuration";

interface SimilarVideoCardProps {
  video: Movie;
}

export default function SimilarVideoCard({ video }: SimilarVideoCardProps) {
  const { data: configuration } = useGetConfigurationQuery(undefined);

  return (
    <Card>
      <div
        style={{
          width: "100%",
          position: "relative",
          paddingTop: "calc(9 / 16 * 100%)",
        }}
      >
        <img
          src={`${configuration?.images.base_url}w780${video.backdrop_path}`}
          style={{
            top: 0,
            height: "100%",
            position: "absolute",
          }}
        />
        <div
          style={{
            top: 10,
            right: 15,
            position: "absolute",
          }}
        >
          <Typography variant="subtitle2">{`${formatMinuteToReadable(
            getRandomNumber(180)
          )}`}</Typography>
        </div>
        <div
          style={{
            left: 0,
            right: 0,
            bottom: 0,
            paddingLeft: "16px",
            paddingRight: "16px",
            paddingBottom: "4px",
            position: "absolute",
          }}
        >
          <MaxLineTypography
            maxLine={1}
            sx={{ width: "80%", fontWeight: 700 }}
            variant="subtitle1"
          >
            {video.title}
          </MaxLineTypography>
        </div>
      </div>
      <CardContent>
        <Stack spacing={1}>
          <Stack direction="row" alignItems="center">
            <div>
              <Typography
                variant="subtitle2"
                sx={{ color: "success.main" }}
              >{`${getRandomNumber(100)}% Match`}</Typography>
              <Stack direction="row" spacing={1} alignItems="center">
                <AgeLimitChip label={`${getRandomNumber(20)}+`} />
                <Typography variant="body2">
                  {video.release_date.substring(0, 4)}
                </Typography>
              </Stack>
            </div>
            <div style={{ flexGrow: 1 }} />
            <NetflixIconButton>
              <AddIcon />
            </NetflixIconButton>
          </Stack>
          <MaxLineTypography maxLine={4} variant="subtitle2">
            {video.overview}
          </MaxLineTypography>
        </Stack>
      </CardContent>
    </Card>
  );
}


================================================
FILE: src/components/VideoCardPortal.tsx
================================================
import { useNavigate } from "react-router-dom";
import Stack from "@mui/material/Stack";
import Card from "@mui/material/Card";
import CardContent from "@mui/material/CardContent";
import Typography from "@mui/material/Typography";
import VolumeUpIcon from "@mui/icons-material/VolumeUp";
import PlayCircleIcon from "@mui/icons-material/PlayCircle";
import ThumbUpOffAltIcon from "@mui/icons-material/ThumbUpOffAlt";
import AddIcon from "@mui/icons-material/Add";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import { Movie } from "src/types/Movie";
import { usePortal } from "src/providers/PortalProvider";
import { useDetailModal } from "src/providers/DetailModalProvider";
import { formatMinuteToReadable, getRandomNumber } from "src/utils/common";
import NetflixIconButton from "./NetflixIconButton";
import MaxLineTypography from "./MaxLineTypography";
import AgeLimitChip from "./AgeLimitChip";
import QualityChip from "./QualityChip";
import GenreBreadcrumbs from "./GenreBreadcrumbs";
import { useGetConfigurationQuery } from "src/store/slices/configuration";
import { MEDIA_TYPE } from "src/types/Common";
import { useGetGenresQuery } from "src/store/slices/genre";
import { MAIN_PATH } from "src/constant";

interface VideoCardModalProps {
  video: Movie;
  anchorElement: HTMLElement;
}

export default function VideoCardModal({
  video,
  anchorElement,
}: VideoCardModalProps) {
  const navigate = useNavigate();

  const { data: configuration } = useGetConfigurationQuery(undefined);
  const { data: genres } = useGetGenresQuery(MEDIA_TYPE.Movie);
  const setPortal = usePortal();
  const rect = anchorElement.getBoundingClientRect();
  const { setDetailType } = useDetailModal();

  return (
    <Card
      onPointerLeave={() => {
        setPortal(null, null);
      }}
      sx={{
        width: rect.width * 1.5,
        height: "100%",
      }}
    >
      <div
        style={{
          width: "100%",
          position: "relative",
          paddingTop: "calc(9 / 16 * 100%)",
        }}
      >
        <img
          src={`${configuration?.images.base_url}w780${video.backdrop_path}`}
          style={{
            top: 0,
            height: "100%",
            objectFit: "cover",
            position: "absolute",
            backgroundPosition: "50%",
          }}
        />
        <div
          style={{
            display: "flex",
            flexDirection: "row",
            alignItems: "center",
            left: 0,
            right: 0,
            bottom: 0,
            paddingLeft: "16px",
            paddingRight: "16px",
            paddingBottom: "4px",
            position: "absolute",
          }}
        >
          <MaxLineTypography
            maxLine={2}
            sx={{ width: "80%", fontWeight: 700 }}
            variant="h6"
          >
            {video.title}
          </MaxLineTypography>
          <div style={{ flexGrow: 1 }} />
          <NetflixIconButton>
            <VolumeUpIcon />
          </NetflixIconButton>
        </div>
      </div>
      <CardContent>
        <Stack spacing={1}>
          <Stack direction="row" spacing={1}>
            <NetflixIconButton
              sx={{ p: 0 }}
              onClick={() => navigate(`/${MAIN_PATH.watch}`)}
            >
              <PlayCircleIcon sx={{ width: 40, height: 40 }} />
            </NetflixIconButton>
            <NetflixIconButton>
              <AddIcon />
            </NetflixIconButton>
            <NetflixIconButton>
              <ThumbUpOffAltIcon />
            </NetflixIconButton>
            <div style={{ flexGrow: 1 }} />
            <NetflixIconButton
              onClick={() => {
                setDetailType({ mediaType: MEDIA_TYPE.Movie, id: video.id });
              }}
            >
              <ExpandMoreIcon />
            </NetflixIconButton>
          </Stack>
          <Stack direction="row" spacing={1} alignItems="center">
            <Typography
              variant="subtitle1"
              sx={{ color: "success.main" }}
            >{`${getRandomNumber(100)}% Match`}</Typography>
            <AgeLimitChip label={`${getRandomNumber(20)}+`} />
            <Typography variant="subtitle2">{`${formatMinuteToReadable(
              getRandomNumber(180)
            )}`}</Typography>
            <QualityChip label="HD" />
          </Stack>
          {genres && (
            <GenreBreadcrumbs
              genres={genres
                .filter((genre) => video.genre_ids.includes(genre.id))
                .map((genre) => genre.name)}
            />
          )}
        </Stack>
      </CardContent>
    </Card>
  );
}


================================================
FILE: src/components/VideoItemWithHover.tsx
================================================
import { useEffect, useState, useRef } from "react";
import { Movie } from "src/types/Movie";
import { usePortal } from "src/providers/PortalProvider";
import { useGetConfigurationQuery } from "src/store/slices/configuration";
import VideoItemWithHoverPure from "./VideoItemWithHoverPure";
interface VideoItemWithHoverProps {
  video: Movie;
}

export default function VideoItemWithHover({ video }: VideoItemWithHoverProps) {
  const setPortal = usePortal();
  const elementRef = useRef<HTMLDivElement>(null);
  const [isHovered, setIsHovered] = useState(false);

  const { data: configuration } = useGetConfigurationQuery(undefined);

  useEffect(() => {
    if (isHovered) {
      setPortal(elementRef.current, video);
    }
  }, [isHovered]);

  return (
    <VideoItemWithHoverPure
      ref={elementRef}
      handleHover={setIsHovered}
      src={`${configuration?.images.base_url}w300${video.backdrop_path}`}
    />
  );
}


================================================
FILE: src/components/VideoItemWithHoverPure.tsx
================================================
import { PureComponent, ForwardedRef, forwardRef } from "react";

type VideoItemWithHoverPureType = {
  src: string;
  innerRef: ForwardedRef<HTMLDivElement>;
  handleHover: (value: boolean) => void;
};

class VideoItemWithHoverPure extends PureComponent<VideoItemWithHoverPureType> {
  render() {
    return (
      <div
        ref={this.props.innerRef}
        style={{
          zIndex: 9,
          cursor: "pointer",
          borderRadius: 0.5,
          width: "100%",
          position: "relative",
          paddingTop: "calc(9 / 16 * 100%)",
        }}
      >
        <img
          src={this.props.src}
          style={{
            top: 0,
            height: "100%",
            objectFit: "cover",
            position: "absolute",
            borderRadius: "4px",
          }}
          onPointerEnter={() => {
            // console.log("onPointerEnter");
            this.props.handleHover(true);
          }}
          onPointerLeave={() => {
            // console.log("onPointerLeave");
            this.props.handleHover(false);
          }}
        />
      </div>
    );
  }
}

const VideoItemWithHoverRef = forwardRef<
  HTMLDivElement,
  Omit<VideoItemWithHoverPureType, "innerRef">
>((props, ref) => <VideoItemWithHoverPure {...props} innerRef={ref} />);
VideoItemWithHoverRef.displayName = "VideoItemWithHoverRef";

export default VideoItemWithHoverRef;


================================================
FILE: src/components/VideoPortalContainer.tsx
================================================
import { useRef } from "react";
import { motion } from "framer-motion";
import Portal from "@mui/material/Portal";

import VideoCardPortal from "./VideoCardPortal";
import MotionContainer from "./animate/MotionContainer";
import {
  varZoomIn,
  varZoomInLeft,
  varZoomInRight,
} from "./animate/variants/zoom/ZoomIn";
import { usePortalData } from "src/providers/PortalProvider";

export default function VideoPortalContainer() {
  const { miniModalMediaData, anchorElement } = usePortalData();
  const container = useRef(null);
  const rect = anchorElement?.getBoundingClientRect();

  const hasToRender = !!miniModalMediaData && !!anchorElement;
  let isFirstElement = false;
  let isLastElement = false;
  let variant = varZoomIn;
  if (hasToRender) {
    const parentElement = anchorElement.closest(".slick-active");
    const nextSiblingOfParentElement = parentElement?.nextElementSibling;
    const previousSiblingOfParentElement =
      parentElement?.previousElementSibling;
    if (!previousSiblingOfParentElement?.classList.contains("slick-active")) {
      isFirstElement = true;
      variant = varZoomInLeft;
    } else if (
      !nextSiblingOfParentElement?.classList.contains("slick-active")
    ) {
      isLastElement = true;
      variant = varZoomInRight;
    }
  }

  return (
    <>
      {hasToRender && (
        <Portal container={container.current}>
          <VideoCardPortal
            video={miniModalMediaData}
            anchorElement={anchorElement}
          />
        </Portal>
      )}
      <MotionContainer open={hasToRender} initial="initial">
        <motion.div
          ref={container}
          variants={variant}
          style={{
            zIndex: 1,
            position: "absolute",
            display: "inline-block",
            ...(rect && {
              top: rect.top + window.pageYOffset - 0.75 * rect.height,
              ...(isLastElement
                ? {
                    right: document.documentElement.clientWidth - rect.right,
                  }
                : {
                    left: isFirstElement
                      ? rect.left
                      : rect.left - 0.25 * rect.width,
                  }),
            }),
          }}
        />
      </MotionContainer>
    </>
  );
}


================================================
FILE: src/components/VideoSlider.tsx
================================================
import withPagination from "src/hoc/withPagination";
import { MEDIA_TYPE } from "src/types/Common";
import { CustomGenre, Genre } from "src/types/Genre";
import SlickSlider from "./slick-slider/SlickSlider";

interface SliderRowForGenreProps {
  genre: Genre | CustomGenre;
  mediaType: MEDIA_TYPE;
}
export default function SliderRowForGenre({
  genre,
  mediaType,
}: SliderRowForGenreProps) {
  const Component = withPagination(SlickSlider, mediaType, genre);
  return <Component />;
}


================================================
FILE: src/components/animate/MotionContainer.tsx
================================================
import { motion } from "framer-motion";
import Box, { BoxProps } from "@mui/material/Box";
import { varWrapBoth } from "./variants/Wrap";

interface MotionContainerProps extends BoxProps {
  initial?: boolean | string;
  open?: boolean;
}

export default function MotionContainer({
  open,
  children,
  ...other
}: MotionContainerProps) {
  return (
    <Box
      initial={false}
      variants={varWrapBoth}
      component={motion.div}
      animate={open ? "animate" : "exit"}
      {...other}
    >
      {children}
    </Box>
  );
}


================================================
FILE: src/components/animate/variants/Wrap.ts
================================================
export const varWrapEnter = {
  animate: {
    transition: { staggerChildren: 0.1 },
  },
};

export const varWrapExit = {
  exit: {
    transition: { staggerChildren: 0.1 },
  },
};

export const varWrapBoth = {
  animate: {
    transition: { staggerChildren: 0.07, delayChildren: 0.1 },
  },
  exit: {
    transition: { staggerChildren: 0.05, staggerDirection: -1 },
  },
};


================================================
FILE: src/components/animate/variants/fade/FadeIn.ts
================================================
const TRANSITION_ENTER = {
  duration: 0.64,
  ease: [0.43, 0.13, 0.23, 0.96],
};
const TRANSITION_EXIT = {
  duration: 0.48,
  ease: [0.43, 0.13, 0.23, 0.96],
};

export const varFadeIn = {
  initial: { opacity: 0 },
  animate: { opacity: 1, transition: TRANSITION_ENTER },
  exit: { opacity: 0, transition: TRANSITION_EXIT },
};


================================================
FILE: src/components/animate/variants/fade/FadeOut.ts
================================================
const TRANSITION_ENTER = {
  duration: 0.64,
  ease: [0.43, 0.13, 0.23, 0.96],
};
const TRANSITION_EXIT = {
  duration: 0.48,
  ease: [0.43, 0.13, 0.23, 0.96],
};

export const varFadeOut = {
  initial: { opacity: 1 },
  animate: { opacity: 0, transition: TRANSITION_ENTER },
  exit: { opacity: 1, transition: TRANSITION_EXIT },
};


================================================
FILE: src/components/animate/variants/zoom/ZoomIn.ts
================================================
const DISTANCE = 0;
const IN = { scale: 1, opacity: 1 };
const OUT = { scale: 0, opacity: 0 };

const TRANSITION_ENTER = {
  duration: 1,
  ease: [0.43, 0.13, 0.23, 0.96],
};

const TRANSITION_EXIT = {
  duration: 1,
  ease: [0.43, 0.13, 0.23, 0.96],
};

export const varZoomIn = {
  initial: OUT,
  animate: { ...IN, transition: TRANSITION_ENTER },
  exit: { ...OUT, transition: TRANSITION_EXIT },
};

export const varZoomInLeft = {
  initial: { ...OUT, translateX: -DISTANCE },
  animate: { ...IN, translateX: 0, transition: TRANSITION_ENTER },
  exit: { ...OUT, translateX: -DISTANCE, transition: TRANSITION_EXIT },
};

export const varZoomInRight = {
  initial: { ...OUT, translateX: DISTANCE },
  animate: { ...IN, translateX: 0, transition: TRANSITION_ENTER },
  exit: { ...OUT, translateX: DISTANCE, transition: TRANSITION_EXIT },
};


================================================
FILE: src/components/layouts/Footer.tsx
================================================
import Box from "@mui/material/Box";
import Link from "@mui/material/Link";
import Typography from "@mui/material/Typography";
import Divider from "@mui/material/Divider";

export default function Footer() {
  return (
    <Box
      component="footer"
      sx={{
        display: "flex",
        flexDirection: "column",
        justifyContent: "center",
        height: 150,
        bgcolor: "inherit",
        px: "60px",
      }}
    >
      <Divider
        component="div"
        sx={{
          "::before, ::after": { top: "0%" },
        }}
      >
        <Typography color="grey.700" variant="h6" component="span">
          Developed by{" "}
          <Link
            target="_blank"
            underline="none"
            sx={{ color: "text.primary" }}
            href="https://github.com/crazy-man22"
          >
            Crazy Man
          </Link>
        </Typography>
      </Divider>
    </Box>
  );
}


================================================
FILE: src/components/layouts/MainHeader.tsx
================================================
import * as React from "react";
import AppBar from "@mui/material/AppBar";
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import Toolbar from "@mui/material/Toolbar";
import IconButton from "@mui/material/IconButton";
import Typography from "@mui/material/Typography";
import Menu from "@mui/material/Menu";
import MenuIcon from "@mui/icons-material/Menu";
import Avatar from "@mui/material/Avatar";
import Tooltip from "@mui/material/Tooltip";
import MenuItem from "@mui/material/MenuItem";
import useOffSetTop from "src/hooks/useOffSetTop";
import { APP_BAR_HEIGHT } from "src/constant";
import Logo from "../Logo";
import SearchBox from "../SearchBox";
import NetflixNavigationLink from "../NetflixNavigationLink";

const pages = ["My List", "Movies", "Tv Shows"];

const MainHeader = () => {
  const isOffset = useOffSetTop(APP_BAR_HEIGHT);

  const [anchorElNav, setAnchorElNav] = React.useState<null | HTMLElement>(
    null
  );
  const [anchorElUser, setAnchorElUser] = React.useState<null | HTMLElement>(
    null
  );

  const handleOpenNavMenu = (event: React.MouseEvent<HTMLElement>) => {
    setAnchorElNav(event.currentTarget);
  };
  const handleOpenUserMenu = (event: React.MouseEvent<HTMLElement>) => {
    setAnchorElUser(event.currentTarget);
  };

  const handleCloseNavMenu = () => {
    setAnchorElNav(null);
  };

  const handleCloseUserMenu = () => {
    setAnchorElUser(null);
  };

  return (
    <AppBar
      sx={{
        // px: "4%",
        px: "60px",
        height: APP_BAR_HEIGHT,
        backgroundImage: "none",
        ...(isOffset
          ? {
              bgcolor: "primary.main",
              boxShadow: (theme) => theme.shadows[4],
            }
          : { boxShadow: 0, bgcolor: "transparent" }),
      }}
    >
      <Toolbar disableGutters>
        <Logo sx={{ mr: { xs: 2, sm: 4 } }} />

        <Box sx={{ flexGrow: 1, display: { xs: "flex", md: "none" } }}>
          <IconButton
            size="large"
            aria-label="account of current user"
            aria-controls="menu-appbar"
            aria-haspopup="true"
            onClick={handleOpenNavMenu}
            color="inherit"
          >
            <MenuIcon />
          </IconButton>
          <Menu
            id="menu-appbar"
            anchorEl={anchorElNav}
            anchorOrigin={{
              vertical: "bottom",
              horizontal: "left",
            }}
            keepMounted
            transformOrigin={{
              vertical: "top",
              horizontal: "left",
            }}
            open={Boolean(anchorElNav)}
            onClose={handleCloseNavMenu}
            sx={{
              display: { xs: "block", md: "none" },
            }}
          >
            {pages.map((page) => (
              <MenuItem key={page} onClick={handleCloseNavMenu}>
                <Typography textAlign="center">{page}</Typography>
              </MenuItem>
            ))}
          </Menu>
        </Box>
        <Typography
          variant="h5"
          noWrap
          component="a"
          href=""
          sx={{
            mr: 2,
            display: { xs: "flex", md: "none" },
            flexGrow: 1,
            fontWeight: 700,
            color: "inherit",
            textDecoration: "none",
          }}
        >
          Netflix
        </Typography>
        <Stack
          direction="row"
          spacing={3}
          sx={{ flexGrow: 1, display: { xs: "none", md: "flex" } }}
        >
          {pages.map((page) => (
            <NetflixNavigationLink
              to=""
              variant="subtitle1"
              key={page}
              onClick={handleCloseNavMenu}
            >
              {page}
            </NetflixNavigationLink>
          ))}
        </Stack>

        <Box sx={{ flexGrow: 0, display: "flex", gap: 2 }}>
          <SearchBox />
          <Tooltip title="Open settings">
            <IconButton onClick={handleOpenUserMenu} sx={{ p: 0 }}>
              <Avatar alt="user_avatar" src="/avatar.png" variant="rounded" />
            </IconButton>
          </Tooltip>
          <Menu
            sx={{ mt: "45px" }}
            id="avatar-menu"
            anchorEl={anchorElUser}
            anchorOrigin={{
              vertical: "top",
              horizontal: "right",
            }}
            keepMounted
            transformOrigin={{
              vertical: "top",
              horizontal: "right",
            }}
            open={Boolean(anchorElUser)}
            onClose={handleCloseUserMenu}
          >
            {["Account", "Logout"].map((setting) => (
              <MenuItem key={setting} onClick={handleCloseUserMenu}>
                <Typography textAlign="center">{setting}</Typography>
              </MenuItem>
            ))}
          </Menu>
        </Box>
      </Toolbar>
    </AppBar>
  );
};
export default MainHeader;


================================================
FILE: src/components/layouts/index.ts
================================================
export { default as MainHeader } from "./MainHeader";
export { default as Footer } from "./Footer";


================================================
FILE: src/components/slick-slider/CustomNavigation.tsx
================================================
import { styled } from "@mui/material/styles";
import Box from "@mui/material/Box";
import ArrowBackIosNewIcon from "@mui/icons-material/ArrowBackIosNew";
import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos";
import { MouseEventHandler, ReactNode } from "react";

const ArrowStyle = styled(Box)(({ theme }) => ({
  top: 0,
  bottom: 0,
  position: "absolute",
  zIndex: 9,
  height: "100%",
  opacity: 0.48,
  display: "flex",
  cursor: "pointer",
  alignItems: "center",
  justifyContent: "center",
  color: theme.palette.common.white,
  // background: theme.palette.grey[700],
  transition: theme.transitions.create("opacity"),
  "&:hover": {
    opacity: 0.8,
    background: theme.palette.grey[900],
  },
  [theme.breakpoints.down("sm")]: {
    display: "none",
  },
}));

interface CustomNaviationProps {
  isEnd: boolean;
  arrowWidth: number;
  children: ReactNode;
  activeSlideIndex: number;
  onNext: MouseEventHandler<HTMLDivElement>;
  onPrevious: MouseEventHandler<HTMLDivElement>;
}

export default function CustomNavigation({
  isEnd,
  onNext,
  children,
  onPrevious,
  arrowWidth,
  activeSlideIndex,
}: CustomNaviationProps) {
  return (
    <>
      {activeSlideIndex > 0 && (
        <ArrowStyle
          onClick={onPrevious}
          sx={{
            left: 0,
            width: { xs: arrowWidth / 2, sm: arrowWidth },
            borderTopRightRadius: { xs: "4px" },
            borderBottomRightRadius: { xs: "4px" },
            // backgroundImage: (theme) =>
            //   `linear-gradient(to right, ${theme.palette.background.default} 0%, rgba(0,0,0,0) 100%)`,
          }}
        >
          <ArrowBackIosNewIcon />
        </ArrowStyle>
      )}

      {children}
      {!isEnd && (
        <ArrowStyle
          onClick={onNext}
          sx={{
            right: 0,
            width: { xs: arrowWidth / 2, sm: arrowWidth },
            borderTopLeftRadius: { xs: "4px" },
            borderBottomLeftRadius: { xs: "4px" },
            // backgroundImage: (theme) =>
            //   `linear-gradient(to left, ${theme.palette.background.default} 0%, rgba(0,0,0,0) 100%)`,
          }}
        >
          <ArrowForwardIosIcon />
        </ArrowStyle>
      )}
    </>
  );
}


================================================
FILE: src/components/slick-slider/SlickSlider.tsx
================================================
import { useState, useRef } from "react";
import Slider, { Settings } from "react-slick";
import { motion } from "framer-motion";

import { styled, Theme, useTheme } from "@mui/material/styles";
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";

import CustomNavigation from "./CustomNavigation";
import VideoItemWithHover from "src/components/VideoItemWithHover";
import { ARROW_MAX_WIDTH } from "src/constant";
import NetflixNavigationLink from "src/components/NetflixNavigationLink";
import MotionContainer from "src/components/animate/MotionContainer";
import { varFadeIn } from "src/components/animate/variants/fade/FadeIn";
import { CustomGenre, Genre } from "src/types/Genre";
import { Movie } from "src/types/Movie";
import { PaginatedMovieResult } from "src/types/Common";

const RootStyle = styled("div")(() => ({
  position: "relative",
  overflow: "inherit",
}));

const StyledSlider = styled(Slider)(
  ({ theme, padding }: { theme: Theme; padding: number }) => ({
    display: "flex !important",
    justifyContent: "center",
    overflow: "initial !important",
    "& > .slick-list": {
      overflow: "visible",
    },
    [theme.breakpoints.up("sm")]: {
      "& > .slick-list": {
        width: `calc(100% - ${2 * padding}px)`,
      },
      "& .slick-list > .slick-track": {
        margin: "0px !important",
      },
      "& .slick-list > .slick-track > .slick-current > div > .NetflixBox-root > .NetflixPaper-root:hover":
        {
          transformOrigin: "0% 50% !important",
        },
    },
    [theme.breakpoints.down("sm")]: {
      "& > .slick-list": {
        width: `calc(100% - ${padding}px)`,
      },
    },
  })
);

interface SlideItemProps {
  item: Movie;
}

function SlideItem({ item }: SlideItemProps) {
  return (
    <Box sx={{ pr: { xs: 0.5, sm: 1 } }}>
      <VideoItemWithHover video={item} />
    </Box>
  );
}

interface SlickSliderProps {
  data: PaginatedMovieResult;
  genre: Genre | CustomGenre;
  handleNext: (page: number) => void;
}
export default function SlickSlider({ data, genre }: SlickSliderProps) {
  const sliderRef = useRef<Slider>(null);
  const [activeSlideIndex, setActiveSlideIndex] = useState(0);
  const [showExplore, setShowExplore] = useState(false);
  const [isEnd, setIsEnd] = useState(false);
  const theme = useTheme();

  const beforeChange = async (currentIndex: number, nextIndex: number) => {
    if (currentIndex < nextIndex) {
      setActiveSlideIndex(nextIndex);
    } else if (currentIndex > nextIndex) {
      setIsEnd(false);
    }
    setActiveSlideIndex(nextIndex);
  };

  const settings: Settings = {
    speed: 500,
    arrows: false,
    infinite: false,
    lazyLoad: "ondemand",
    slidesToShow: 6,
    slidesToScroll: 6,
    // afterChange: (current) => {
    //   console.log("After Change", current);
    // },
    beforeChange,
    // onEdge: (direction) => {
    //   console.log("Edge: ", direction);
    // },
    responsive: [
      {
        breakpoint: 1536,
        settings: {
          slidesToShow: 5,
          slidesToScroll: 5,
        },
      },
      {
        breakpoint: 1200,
        settings: {
          slidesToShow: 4,
          slidesToScroll: 4,
        },
      },
      {
        breakpoint: 900,
        settings: {
          slidesToShow: 3,
          slidesToScroll: 3,
        },
      },
      {
        breakpoint: 600,
        settings: {
          slidesToShow: 2,
          slidesToScroll: 2,
        },
      },
    ],
  };

  const handlePrevious = () => {
    sliderRef.current?.slickPrev();
  };

  const handleNext = () => {
    sliderRef.current?.slickNext();
  };

  return (
    <Box sx={{ overflow: "hidden", height: "100%", zIndex: 1 }}>
      {data.results.length > 0 && (
        <>
          <Stack
            spacing={2}
            direction="row"
            alignItems="center"
            sx={{ mb: 2, pl: { xs: "30px", sm: "60px" } }}
          >
            <NetflixNavigationLink
              variant="h5"
              to={`/genre/${
                genre.id || genre.name.toLowerCase().replace(" ", "_")
              }`}
              sx={{
                display: "inline-block",
                fontWeight: 700,
              }}
              onMouseOver={() => {
                setShowExplore(true);
              }}
              onMouseLeave={() => {
                setShowExplore(false);
              }}
            >
              {`${genre.name} Movies `}
              <MotionContainer
                open={showExplore}
                initial="initial"
                sx={{ display: "inline", color: "success.main" }}
              >
                {"Explore All".split("").map((letter, index) => (
                  <motion.span key={index} variants={varFadeIn}>
                    {letter}
                  </motion.span>
                ))}
              </MotionContainer>
            </NetflixNavigationLink>
          </Stack>

          <RootStyle>
            <CustomNavigation
              isEnd={isEnd}
              arrowWidth={ARROW_MAX_WIDTH}
              onNext={handleNext}
              onPrevious={handlePrevious}
              activeSlideIndex={activeSlideIndex}
            >
              <StyledSlider
                ref={sliderRef}
                {...settings}
                padding={ARROW_MAX_WIDTH}
                theme={theme}
              >
                {data.results
                  .filter((i) => !!i.backdrop_path)
                  .map((item) => (
                    <SlideItem key={item.id} item={item} />
                  ))}
              </StyledSlider>
            </CustomNavigation>
          </RootStyle>
        </>
      )}
    </Box>
  );
}


================================================
FILE: src/components/watch/PlayerControlButton.tsx
================================================
import { forwardRef } from "react";

import IconButton, { IconButtonProps } from "@mui/material/IconButton";

const PlayerControlButton = forwardRef<HTMLButtonElement, IconButtonProps>(
  ({ children, ...others }, ref) => (
    <IconButton
      ref={ref}
      sx={{
        padding: { xs: 0.5, sm: 1 },
        "& svg, & span": { transition: "transform .3s" },
        "&:hover svg, &:hover span": {
          msTransform: "scale(1.3)",
          WebkitTransform: "scale(1.3)",
          transform: "scale(1.3)",
        },
      }}
      {...others}
    >
      {children}
    </IconButton>
  )
);

export default PlayerControlButton;


================================================
FILE: src/components/watch/PlayerSeekbar.tsx
================================================
import Slider from "@mui/material/Slider";
import { styled } from "@mui/material/styles";

import { formatTime } from "src/utils/common";

const StyledSlider = styled(Slider)({
  borderRadius: 0,
  "& .NetflixSlider-track": {
    backgroundColor: "red !important",
    border: 0,
  },
  "& .NetflixSlider-rail": {
    border: "none",
    backgroundColor: "white !important",
    opacity: 0.85,
  },
  "& .NetflixSlider-thumb": {
    borderRadius: "50%",
    height: 10,
    width: 10,
    backgroundColor: "red",
    "&:focus, &:hover, &.Netflix-active, &.Netflix-focusVisible": {
      boxShadow: "inherit",
      height: 15,
      width: 15,
    },
    "&:before": {
      display: "none",
      boxShadow: "0 2px 2px 0 #fff",
      height: 10,
      width: 10,
    },
  },
  // "& .NetflixSlider-valueLabel": {
  //   lineHeight: 1.2,
  //   fontSize: 12,
  //   background: "unset",
  //   padding: 0,
  //   width: 32,
  //   height: 32,
  //   borderRadius: "50% 50% 50% 0",
  //   backgroundColor: "#52af77",
  //   transformOrigin: "bottom left",
  //   transform: "translate(50%, -100%) rotate(-45deg) scale(0)",
  //   "&:before": { display: "none" },
  //   "&.NetflixSlider-valueLabelOpen": {
  //     transform: "translate(50%, -100%) rotate(-45deg) scale(1)",
  //   },
  //   "& > *": {
  //     transform: "rotate(45deg)",
  //   },
  // },
});

function PlayerSeekbar({
  playedSeconds,
  duration,
  seekTo,
}: {
  playedSeconds: number;
  duration: number;
  seekTo: (value: number) => void;
}) {
  return (
    <StyledSlider
      valueLabelDisplay="auto"
      valueLabelFormat={(v) => formatTime(v)}
      // components={{
      //   ValueLabel: ValueLabelComponent,
      // }}
      value={playedSeconds}
      max={duration}
      onChange={(_, value) => {
        seekTo(value as number);
      }}
    />
  );
}

export default PlayerSeekbar;


================================================
FILE: src/components/watch/VideoJSPlayer.tsx
================================================
import { useEffect, useRef } from "react";
import Player from "video.js/dist/types/player";
import videojs from "video.js";
import "videojs-youtube";
import "video.js/dist/video-js.css";

export default function VideoJSPlayer({
  options,
  onReady,
}: {
  options: any;
  onReady: (player: Player) => void;
}) {
  const videoRef = useRef<HTMLDivElement | null>(null);
  const playerRef = useRef<Player | null>(null);

  useEffect(() => {
    (async function handleVideojs() {
      // Make sure Video.js player is only initialized once
      if (!playerRef.current) {
        // The Video.js player needs to be _inside_ the component el for React 18 Strict Mode.
        const videoElement = document.createElement("video-js");
        // videoElement.classList.add("vjs-big-play-centered", "vjs-16-9");

        videoRef.current?.appendChild(videoElement);
        const player = (playerRef.current = videojs(
          videoElement,
          options,
          () => {
            onReady && onReady(player);
          }
        ));

        // import("video.js").then(async ({ default: videojs }) => {
        //   await import("video.js/dist/video-js.css");
        //   if (options["techOrder"] && options["techOrder"].includes("youtube")) {
        //     // eslint-disable-next-line
        //     await import("videojs-youtube");
        //   }
        //   const player = (playerRef.current = videojs(
        //     videoElement,
        //     options,
        //     () => {
        //       onReady && onReady(player);
        //     }
        //   ));
        // });

        // await import("video.js/dist/video-js.css");
        // const videojs = await import("video.js");
        // if (options["techOrder"] && options["techOrder"].includes("youtube")) {
        //   // eslint-disable-next-line
        //   await import("videojs-youtube");
        // }
        // const player = (playerRef.current = videojs.default(
        //   videoElement,
        //   options,
        //   () => {
        //     onReady && onReady(player);
        //   }
        // ));

        // You could update an existing player in the `else` block here
        // on prop change, for example:
      } else {
        const player = playerRef.current;
        // player.autoplay(options.autoplay);
        player.width(options.width);
        player.height(options.height);
      }
    })();
  }, [options, videoRef]);

  // Dispose the Video.js player when the functional component unmounts
  useEffect(() => {
    const player = playerRef.current;

    return () => {
      if (player && !player.isDisposed()) {
        player.dispose();
        playerRef.current = null;
      }
    };
  }, [playerRef]);

  return (
    <div data-vjs-player>
      <div ref={videoRef} />
    </div>
  );
}


================================================
FILE: src/components/watch/VolumeControllers.tsx
================================================
import { Stack } from "@mui/material";
import Slider from "@mui/material/Slider";
import { styled } from "@mui/material/styles";
import { SliderUnstyledOwnProps } from "@mui/base/SliderUnstyled";
import VolumeUpIcon from "@mui/icons-material/VolumeUp";
import VolumeOffIcon from "@mui/icons-material/VolumeOff";
import PlayerControlButton from "./PlayerControlButton";

const StyledSlider = styled(Slider)({
  height: 5,
  borderRadius: 0,
  padding: 0,
  "& .NetflixSlider-track": {
    border: "none",
    backgroundColor: "red",
  },
  "& .NetflixSlider-rail": {
    border: "none",
    backgroundColor: "white",
    opacity: 0.85,
  },
  "& .NetflixSlider-thumb": {
    height: 10,
    width: 10,
    backgroundColor: "red",
    "&:focus, &:hover, &.Netflix-active, &.Netflix-focusVisible": {
      boxShadow: "inherit",
      height: 15,
      width: 15,
    },
    "&:before": {
      display: "none",
    },
  },
});

export default function VolumeControllers({
  value,
  handleVolume,
  handleVolumeToggle,
  muted,
}: {
  value: number;
  handleVolume: SliderUnstyledOwnProps["onChange"];
  handleVolumeToggle: React.MouseEventHandler<HTMLButtonElement>;
  muted: boolean;
}) {
  return (
    <Stack
      direction="row"
      alignItems="center"
      spacing={{ xs: 0.5, sm: 1 }}
      // sx={{
      //   "&:hover NetflixSlider-root": {
      //     display: "inline-block",
      //   },
      // }}
    >
      <PlayerControlButton onClick={handleVolumeToggle}>
        {!muted ? <VolumeUpIcon /> : <VolumeOffIcon />}
      </PlayerControlButton>
      <StyledSlider
        max={100}
        value={value * 100}
        valueLabelDisplay="auto"
        valueLabelFormat={(x: number) => x}
        onChange={handleVolume}
        sx={{ width: { xs: 60, sm: 80, md: 100 } }}
      />
    </Stack>
  );
}


================================================
FILE: src/constant/index.ts
================================================
import { CustomGenre } from "src/types/Genre";

export const API_ENDPOINT_URL = import.meta.env.VITE_APP_API_ENDPOINT_URL;
export const TMDB_V3_API_KEY = import.meta.env.VITE_APP_TMDB_V3_API_KEY;

export const MAIN_PATH = {
  root: "",
  browse: "browse",
  genreExplore: "genre",
  watch: "watch",
};

export const ARROW_MAX_WIDTH = 60;
export const COMMON_TITLES: CustomGenre[] = [
  { name: "Popular", apiString: "popular" },
  { name: "Top Rated", apiString: "top_rated" },
  { name: "Now Playing", apiString: "now_playing" },
  { name: "Upcoming", apiString: "upcoming" },
];

export const YOUTUBE_URL = "https://www.youtube.com/watch?v=";
export const APP_BAR_HEIGHT = 70;

export const INITIAL_DETAIL_STATE = {
  id: undefined,
  mediaType: undefined,
  mediaDetail: undefined,
};


================================================
FILE: src/hoc/withPagination.tsx
================================================
import { ElementType, useCallback, useEffect } from "react";
import MainLoadingScreen from "src/components/MainLoadingScreen";
import { useAppDispatch, useAppSelector } from "src/hooks/redux";
import {
  initiateItem,
  useLazyGetVideosByMediaTypeAndGenreIdQuery,
  useLazyGetVideosByMediaTypeAndCustomGenreQuery,
} from "src/store/slices/discover";
import { MEDIA_TYPE } from "src/types/Common";
import { CustomGenre, Genre } from "src/types/Genre";

export default function withPagination(
  Component: ElementType,
  mediaType: MEDIA_TYPE,
  genre: Genre | CustomGenre
) {
  return function WithPagination() {
    const dispatch = useAppDispatch();
    const itemKey = genre.id ?? (genre as CustomGenre).apiString;
    const mediaState = useAppSelector((state) => state.discover[mediaType]);
    const pageState = mediaState ? mediaState[itemKey] : undefined;
    const [getVideosByMediaTypeAndGenreId] =
      useLazyGetVideosByMediaTypeAndGenreIdQuery();
    const [getVideosByMediaTypeAndCustomGenre] =
      useLazyGetVideosByMediaTypeAndCustomGenreQuery();

    useEffect(() => {
      if (!mediaState || !pageState) {
        dispatch(initiateItem({ mediaType, itemKey }));
      }
    }, [mediaState, pageState]);

    useEffect(() => {
      if (pageState && pageState.page === 0) {
        handleNext(pageState.page + 1);
      }
    }, [pageState]);

    const handleNext = useCallback((page: number) => {
      if (genre.id) {
        getVideosByMediaTypeAndGenreId({
          mediaType,
          genreId: genre.id,
          page,
        });
      } else {
        getVideosByMediaTypeAndCustomGenre({
          mediaType,
          apiString: (genre as CustomGenre).apiString,
          page,
        });
      }
      // dispatch(setNextPage({ mediaType, itemKey }));
    }, []);

    if (pageState) {
      return (
        <Component genre={genre} data={pageState} handleNext={handleNext} />
      );
    }
    return <MainLoadingScreen />;
  };
}


================================================
FILE: src/hooks/redux.ts
================================================
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import type { RootState, AppDispatch } from "../store";

// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;


================================================
FILE: src/hooks/useIntersectionObserver.ts
================================================
import { RefObject, useEffect, useState } from "react";
// import { buildThresholdList } from "src/utils";

export default function useIntersectionObserver(
  ref: RefObject<HTMLElement>,
  options?: IntersectionObserverInit
) {
  const [entry, setEntry] = useState<IntersectionObserverEntry | null>(null);

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        setEntry(entry);
      },
      options ?? {
        root: null,
        rootMargin: "0px",
        // threshold: buildThresholdList(),
        threshold: 1,
      }
    );

    if (ref.current) {
      observer.observe(ref.current);
    }

    return () => {
      if (ref.current) {
        observer.unobserve(ref.current);
      }
    };
  }, []);

  return entry;
}


================================================
FILE: src/hooks/useOffSetTop.ts
================================================
import { useState, useEffect, useCallback } from "react";

export default function useOffSetTop(top: number) {
  const [offsetTop, setOffSetTop] = useState(false);
  const onScroll = useCallback(() => {
    if (window.pageYOffset > top) {
      setOffSetTop(true);
    } else {
      setOffSetTop(false);
    }
  }, [top]);

  useEffect(() => {
    window.addEventListener("scroll", onScroll);
    return () => {
      window.removeEventListener("scroll", onScroll);
    };
  }, [top]);

  return offsetTop;
}


================================================
FILE: src/hooks/useWindowSize.ts
================================================
import { useState, useEffect } from "react";

export default function useWindowSize() {
  // Initialize state with undefined width/height so server and client renders match
  // Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/
  const [windowSize, setWindowSize] = useState<{
    width: number | undefined;
    height: number | undefined;
  }>({
    width: undefined,
    height: undefined,
  });

  useEffect(() => {
    // Handler to call on window resize
    function handleResize() {
      // Set window width/height to state
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    }
    // Add event listener
    window.addEventListener("resize", handleResize);
    // Call handler right away so state gets updated with initial window size
    handleResize();
    // Remove event listener on cleanup
    return () => window.removeEventListener("resize", handleResize);
  }, []); // Empty array ensures that effect is only run on mount

  return windowSize;
}


================================================
FILE: src/layouts/MainLayout.tsx
================================================
import { Outlet, useLocation, useNavigation } from "react-router-dom";
import Box from "@mui/material/Box";

import DetailModal from "src/components/DetailModal";
import VideoPortalContainer from "src/components/VideoPortalContainer";
import DetailModalProvider from "src/providers/DetailModalProvider";
import PortalProvider from "src/providers/PortalProvider";
import { MAIN_PATH } from "src/constant";
import { Footer, MainHeader } from "src/components/layouts";
import MainLoadingScreen from "src/components/MainLoadingScreen";

export default function MainLayout() {
  const location = useLocation();
  const navigation = useNavigation();
  // console.log("Nav Stat: ", navigation.state);
  return (
    <Box
      sx={{
        width: "100%",
        minHeight: "100vh",
        bgcolor: "background.default",
      }}
    >
      <MainHeader />
      {navigation.state !== "idle" && <MainLoadingScreen />}
      <DetailModalProvider>
        <DetailModal />
        <PortalProvider>
          {/* <MainLoadingScreen /> */}
          <Outlet />
          <VideoPortalContainer />
        </PortalProvider>
      </DetailModalProvider>
      {location.pathname !== `/${MAIN_PATH.watch}` && <Footer />}
    </Box>
  );
}


================================================
FILE: src/lib/createSafeContext.ts
================================================
import React from "react";

export default function createSafeContext<TValue extends {} | null>() {
  const context = React.createContext<TValue | undefined>(undefined);

  function useContext() {
    const value = React.useContext(context);
    if (value === undefined) {
      throw new Error("useContext must be inside a Provider with a value");
    }
    return value;
  }

  return [useContext, context.Provider] as const;
}


================================================
FILE: src/main.tsx
================================================
import "slick-carousel/slick/slick.css";
import "slick-carousel/slick/slick-theme.css";
import "./CustomClassNameSetup";
import React from "react";
import ReactDOM from "react-dom/client";
import { Provider } from "react-redux";
import { RouterProvider } from "react-router-dom";
import { createTheme, ThemeProvider } from "@mui/material/styles";

import store from "./store";
import { extendedApi } from "./store/slices/configuration";
import palette from "./theme/palette";
import router from "./routes";
import MainLoadingScreen from "./components/MainLoadingScreen";

store.dispatch(extendedApi.endpoints.getConfiguration.initiate(undefined));

const root = ReactDOM.createRoot(
  document.getElementById("root") as HTMLElement
);
root.render(
  <Provider store={store}>
    <React.StrictMode>
      <ThemeProvider theme={createTheme({ palette })}>
        <RouterProvider
          router={router}
          fallbackElement={<MainLoadingScreen />}
        />
      </ThemeProvider>
    </React.StrictMode>
  </Provider>
);


================================================
FILE: src/pages/GenreExplore.tsx
================================================
import {
  LoaderFunctionArgs,
  useLoaderData,
  // useParams
} from "react-router-dom";
import { COMMON_TITLES } from "src/constant";
import GridPage from "src/components/GridPage";
import { MEDIA_TYPE } from "src/types/Common";
import { CustomGenre, Genre } from "src/types/Genre";
import {
  genreSliceEndpoints,
  // useGetGenresQuery
} from "src/store/slices/genre";
import store from "src/store";

export async function loader({ params }: LoaderFunctionArgs) {
  let genre: CustomGenre | Genre | undefined = COMMON_TITLES.find(
    (t) => t.apiString === (params.genreId as string)
  );
  if (!genre) {
    const genres = await store
      .dispatch(genreSliceEndpoints.getGenres.initiate(MEDIA_TYPE.Movie))
      .unwrap();
    genre = genres?.find((t) => t.id.toString() === (params.genreId as string));
  }

  return genre;
}

export function Component() {
  const genre: CustomGenre | Genre | undefined = useLoaderData() as
    | CustomGenre
    | Genre
    | undefined;
  // const { genreId } = useParams();
  // const { data: genres } = useGetGenresQuery(MEDIA_TYPE.Movie);
  // let genre: Genre | CustomGenre | undefined;
  // if (isNaN(parseInt(genreId!))) {
  //   genre = COMMON_TITLES.find((t) => t.apiString === genreId);
  // } else {
  //   genre = genres?.find((t) => t.id.toString() === genreId);
  // }
  if (genre) {
    return <GridPage mediaType={MEDIA_TYPE.Movie} genre={genre} />;
  }
  return null;
}

Component.displayName = "GenreExplore";


================================================
FILE: src/pages/HomePage.tsx
================================================
import Stack from "@mui/material/Stack";
import { COMMON_TITLES } from "src/constant";
import HeroSection from "src/components/HeroSection";
import { genreSliceEndpoints, useGetGenresQuery } from "src/store/slices/genre";
import { MEDIA_TYPE } from "src/types/Common";
import { CustomGenre, Genre } from "src/types/Genre";
import SliderRowForGenre from "src/components/VideoSlider";
import store from "src/store";

export async function loader() {
  await store.dispatch(
    genreSliceEndpoints.getGenres.initiate(MEDIA_TYPE.Movie)
  );
  return null;
}
export function Component() {
  const { data: genres, isSuccess } = useGetGenresQuery(MEDIA_TYPE.Movie);

  if (isSuccess && genres && genres.length > 0) {
    return (
      <Stack spacing={2}>
        <HeroSection mediaType={MEDIA_TYPE.Movie} />
        {[...COMMON_TITLES, ...genres].map((genre: Genre | CustomGenre) => (
          <SliderRowForGenre
            key={genre.id || genre.name}
            genre={genre}
            mediaType={MEDIA_TYPE.Movie}
          />
        ))}
      </Stack>
    );
  }
  return null;
}

Component.displayName = "HomePage";


================================================
FILE: src/pages/WatchPage.tsx
================================================
import { useState, useRef, useMemo } from "react";
import { useNavigate } from "react-router-dom";
import Player from "video.js/dist/types/player";
import { Box, Stack, Typography } from "@mui/material";
import { SliderUnstyledOwnProps } from "@mui/base/SliderUnstyled";
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import PauseIcon from "@mui/icons-material/Pause";
import SkipNextIcon from "@mui/icons-material/SkipNext";
import FullscreenIcon from "@mui/icons-material/Fullscreen";
import SettingsIcon from "@mui/icons-material/Settings";
import BrandingWatermarkOutlinedIcon from "@mui/icons-material/BrandingWatermarkOutlined";
import KeyboardBackspaceIcon from "@mui/icons-material/KeyboardBackspace";

import useWindowSize from "src/hooks/useWindowSize";
import { formatTime } from "src/utils/common";

import MaxLineTypography from "src/components/MaxLineTypography";
import VolumeControllers from "src/components/watch/VolumeControllers";
import VideoJSPlayer from "src/components/watch/VideoJSPlayer";
import PlayerSeekbar from "src/components/watch/PlayerSeekbar";
import PlayerControlButton from "src/components/watch/PlayerControlButton";
import MainLoadingScreen from "src/components/MainLoadingScreen";

export function Component() {
  const playerRef = useRef<Player | null>(null);
  const [playerState, setPlayerState] = useState({
    paused: false,
    muted: false,
    playedSeconds: 0,
    duration: 0,
    volume: 0.8,
    loaded: 0,
  });

  const navigate = useNavigate();
  const [playerInitialized, setPlayerInitialized] = useState(false);

  const windowSize = useWindowSize();
  const videoJsOptions = useMemo(() => {
    return {
      preload: "metadata",
      autoplay: true,
      controls: false,
      // responsive: true,
      // fluid: true,
      width: windowSize.width,
      height: windowSize.height,
      sources: [
        {
          // src: videoData?.video,
          // src: "https://d2zihajmogu5jn.cloudfront.net/bipbop-advanced/bipbop_16x9_variant.m3u8",
          src: "https://bitmovin-a.akamaihd.net/content/sintel/hls/playlist.m3u8",
          type: "application/x-mpegurl",
        },
      ],
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [windowSize]);

  const handlePlayerReady = function (player: Player): void {
    player.on("pause", () => {
      setPlayerState((draft) => {
        return { ...draft, paused: true };
      });
    });

    player.on("play", () => {
      setPlayerState((draft) => {
        return { ...draft, paused: false };
      });
    });

    player.on("timeupdate", () => {
      setPlayerState((draft) => {
        return { ...draft, playedSeconds: player.currentTime() };
      });
    });

    player.one("durationchange", () => {
      setPlayerInitialized(true);
      setPlayerState((draft) => ({ ...draft, duration: player.duration() }));
    });

    playerRef.current = player;

    setPlayerState((draft) => {
      return { ...draft, paused: player.paused() };
    });
  };

  const handleVolumeChange: SliderUnstyledOwnProps["onChange"] = (_, value) => {
    playerRef.current?.volume((value as number) / 100);
    setPlayerState((draft) => {
      return { ...draft, volume: (value as number) / 100 };
    });
  };

  const handleSeekTo = (v: number) => {
    playerRef.current?.currentTime(v);
  };

  const handleGoBack = () => {
    navigate("/browse");
  };

  if (!!videoJsOptions.width) {
    return (
      <Box
        sx={{
          position: "relative",
        }}
      >
        <VideoJSPlayer options={videoJsOptions} onReady={handlePlayerReady} />
        {playerRef.current && playerInitialized && (
          <Box
            sx={{
              top: 0,
              left: 0,
              right: 0,
              bottom: 0,
              position: "absolute",
            }}
          >
            <Box px={2} sx={{ position: "absolute", top: 75 }}>
              <PlayerControlButton onClick={handleGoBack}>
                <KeyboardBackspaceIcon />
              </PlayerControlButton>
            </Box>
            <Box
              px={2}
              sx={{
                position: "absolute",
                top: { xs: "40%", sm: "55%", md: "60%" },
                left: 0,
              }}
            >
              <Typography
                variant="h3"
                sx={{
                  fontWeight: 700,
                  color: "white",
                }}
              >
                Title
              </Typography>
            </Box>
            <Box
              px={{ xs: 0, sm: 1, md: 2 }}
              sx={{
                position: "absolute",
                top: { xs: "50%", sm: "60%", md: "70%" },
                right: 0,
              }}
            >
              <Typography
                variant="subtitle2"
                sx={{
                  px: 1,
                  py: 0.5,
                  fontWeight: 700,
                  color: "white",
                  bgcolor: "red",
                  borderRadius: "12px 0px 0px 12px",
                }}
              >
                12+
              </Typography>
            </Box>

            <Box
              px={{ xs: 1, sm: 2 }}
              sx={{ position: "absolute", bottom: 20, left: 0, right: 0 }}
            >
              {/* Seekbar */}
              <Stack direction="row" alignItems="center" spacing={1}>
                <PlayerSeekbar
                  playedSeconds={playerState.playedSeconds}
                  duration={playerState.duration}
                  seekTo={handleSeekTo}
                />
              </Stack>
              {/* end Seekbar */}

              {/* Controller */}
              <Stack direction="row" alignItems="center">
                {/* left controller */}
                <Stack
                  direction="row"
                  spacing={{ xs: 0.5, sm: 1.5, md: 2 }}
                  alignItems="center"
                >
                  {!playerState.paused ? (
                    <PlayerControlButton
                      onClick={() => {
                        playerRef.current?.pause();
                      }}
                    >
                      <PauseIcon />
                    </PlayerControlButton>
                  ) : (
                    <PlayerControlButton
                      onClick={() => {
                        playerRef.current?.play();
                      }}
                    >
                      <PlayArrowIcon />
                    </PlayerControlButton>
                  )}
                  <PlayerControlButton>
                    <SkipNextIcon />
                  </PlayerControlButton>
                  <VolumeControllers
                    muted={playerState.muted}
                    handleVolumeToggle={() => {
                      playerRef.current?.muted(!playerState.muted);
                      setPlayerState((draft) => {
                        return { ...draft, muted: !draft.muted };
                      });
                    }}
                    value={playerState.volume}
                    handleVolume={handleVolumeChange}
                  />
                  <Typography variant="caption" sx={{ color: "white" }}>
                    {`${formatTime(playerState.playedSeconds)} / ${formatTime(
                      playerState.duration
                    )}`}
                  </Typography>
                </Stack>
                {/* end left controller */}

                {/* middle time */}
                <Box flexGrow={1}>
                  <MaxLineTypography
                    maxLine={1}
                    variant="subtitle1"
                    textAlign="center"
                    sx={{ maxWidth: 300, mx: "auto", color: "white" }}
                  >
                    Description
                  </MaxLineTypography>
                </Box>
                {/* end middle time */}

                {/* right controller */}
                <Stack
                  direction="row"
                  alignItems="center"
                  spacing={{ xs: 0.5, sm: 1.5, md: 2 }}
                >
                  <PlayerControlButton>
                    <SettingsIcon />
                  </PlayerControlButton>
                  <PlayerControlButton>
                    <BrandingWatermarkOutlinedIcon />
                  </PlayerControlButton>
                  <PlayerControlButton>
                    <FullscreenIcon />
                  </PlayerControlButton>
                </Stack>
                {/* end right controller */}
              </Stack>
              {/* end Controller */}
            </Box>
          </Box>
        )}
      </Box>
    );
  }
  return null;
}

Component.displayName = "WatchPage";


================================================
FILE: src/providers/DetailModalProvider.tsx
================================================
import { ReactNode, useEffect, useState, useCallback } from "react";
import { useLocation } from "react-router-dom";

import { INITIAL_DETAIL_STATE } from "src/constant";
import createSafeContext from "src/lib/createSafeContext";
import { useLazyGetAppendedVideosQuery } from "src/store/slices/discover";
import { MEDIA_TYPE } from "src/types/Common";
import { MovieDetail } from "src/types/Movie";

interface DetailType {
  id?: number;
  mediaType?: MEDIA_TYPE;
}
export interface DetailModalConsumerProps {
  detail: { mediaDetail?: MovieDetail } & DetailType;
  setDetailType: (newDetailType: DetailType) => void;
}

export const [useDetailModal, Provider] =
  createSafeContext<DetailModalConsumerProps>();

export default function DetailModalProvider({
  children,
}: {
  children: ReactNode;
}) {
  const location = useLocation();
  const [detail, setDetail] = useState<
    { mediaDetail?: MovieDetail } & DetailType
  >(INITIAL_DETAIL_STATE);

  const [getAppendedVideos] = useLazyGetAppendedVideosQuery();

  const handleChangeDetail = useCallback(
    async (newDetailType: { mediaType?: MEDIA_TYPE; id?: number }) => {
      if (!!newDetailType.id && newDetailType.mediaType) {
        const response = await getAppendedVideos({
          mediaType: newDetailType.mediaType,
          id: newDetailType.id as number,
        }).unwrap();
        setDetail({ ...newDetailType, mediaDetail: response });
      } else {
        setDetail(INITIAL_DETAIL_STATE);
      }
    },
    []
  );

  useEffect(() => {
    setDetail(INITIAL_DETAIL_STATE);
  }, [location.pathname, setDetail]);

  return (
    <Provider value={{ detail, setDetailType: handleChangeDetail }}>
      {children}
    </Provider>
  );
}


================================================
FILE: src/providers/PortalProvider.tsx
================================================
import { ReactNode, useState, useCallback } from "react";
import { Movie } from "src/types/Movie";
import createSafeContext from "src/lib/createSafeContext";

export interface PortalConsumerProps {
  setPortal: (anchor: HTMLElement | null, vidoe: Movie | null) => void;
}
export interface PortalDataConsumerProps {
  anchorElement: HTMLElement | null;
  miniModalMediaData: Movie | null;
}

export const [usePortal, Provider] =
  createSafeContext<PortalConsumerProps["setPortal"]>();

export const [usePortalData, PortalDataProvider] =
  createSafeContext<PortalDataConsumerProps>();

export default function PortalProvider({ children }: { children: ReactNode }) {
  const [anchorElement, setAnchorElement] = useState<HTMLElement | null>(null);
  const [miniModalMediaData, setMiniModalMediaData] = useState<Movie | null>(
    null
  );

  const handleChangePortal = useCallback(
    (anchor: HTMLElement | null, video: Movie | null) => {
      setAnchorElement(anchor);
      setMiniModalMediaData(video);
    },
    []
  );

  return (
    <Provider
      value={handleChangePortal}
    >
      <PortalDataProvider
        value={{
          anchorElement,
          miniModalMediaData,
        }}
      >
        {children}
      </PortalDataProvider>
    </Provider>
  );
}


================================================
FILE: src/routes/index.tsx
================================================
import { Navigate, createBrowserRouter } from "react-router-dom";
import { MAIN_PATH } from "src/constant";

import MainLayout from "src/layouts/MainLayout";

const router = createBrowserRouter([
  {
    path: "/",
    element: <MainLayout />,
    children: [
      {
        path: MAIN_PATH.root,
        element: <Navigate to={`/${MAIN_PATH.browse}`} />,
      },
      {
        path: MAIN_PATH.browse,
        lazy: () => import("src/pages/HomePage"),
      },
      {
        path: MAIN_PATH.genreExplore,
        children: [
          {
            path: ":genreId",
            lazy: () => import("src/pages/GenreExplore"),
          },
        ],
      },
      {
        path: MAIN_PATH.watch,
        lazy: () => import("src/pages/WatchPage"),
      },
    ],
  },
]);

export default router;


================================================
FILE: src/store/index.ts
================================================
import { configureStore } from "@reduxjs/toolkit";
import { tmdbApi } from "./slices/apiSlice";
import discoverReducer from "./slices/discover";

const store = configureStore({
  reducer: {
    discover: discoverReducer,
    [tmdbApi.reducerPath]: tmdbApi.reducer,
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(tmdbApi.middleware),
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

export default store;


================================================
FILE: src/store/slices/apiSlice.ts
================================================
import { API_ENDPOINT_URL } from "src/constant";
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";

export const tmdbApi = createApi({
  reducerPath: "tmdbApi",
  baseQuery: fetchBaseQuery({ baseUrl: API_ENDPOINT_URL }),
  endpoints: (build) => ({}),
});


================================================
FILE: src/store/slices/configuration.ts
================================================
import { TMDB_V3_API_KEY } from "src/constant";
import { tmdbApi } from "./apiSlice";

type ConfigurationType = {
  images: {
    base_url: string;
    secure_base_url: string;
    backdrop_sizes: string[];
    logo_sizes: string[];
    poster_sizes: string[];
    profile_sizes: string[];
    still_sizes: string[];
  };
  change_keys: string[];
};

export const extendedApi = tmdbApi.injectEndpoints({
  endpoints: (build) => ({
    getConfiguration: build.query<ConfigurationType, undefined>({
      query: () => ({
        url: "/configuration",
        params: { api_key: TMDB_V3_API_KEY },
      }),
    }),
  }),
});

export const { useGetConfigurationQuery } = extendedApi;


================================================
FILE: src/store/slices/discover.ts
================================================
import { TMDB_V3_API_KEY } from "src/constant";
import { tmdbApi } from "./apiSlice";
import { MEDIA_TYPE, PaginatedMovieResult } from "src/types/Common";
import { MovieDetail } from "src/types/Movie";
import { createSlice, isAnyOf } from "@reduxjs/toolkit";

const initialState: Record<string, Record<string, PaginatedMovieResult>> = {};
export const initialItemState: PaginatedMovieResult = {
  page: 0,
  results: [],
  total_pages: 0,
  total_results: 0,
};

const discoverSlice = createSlice({
  name: "discover",
  initialState,
  reducers: {
    setNextPage: (state, action) => {
      const { mediaType, itemKey } = action.payload;
      state[mediaType][itemKey].page += 1;
    },
    initiateItem: (state, action) => {
      const { mediaType, itemKey } = action.payload;
      if (!state[mediaType]) {
        state[mediaType] = {};
      }
      if (!state[mediaType][itemKey]) {
        state[mediaType][itemKey] = initialItemState;
      }
    },
  },
  extraReducers(builder) {
    builder.addMatcher(
      isAnyOf(
        extendedApi.endpoints.getVideosByMediaTypeAndCustomGenre.matchFulfilled,
        extendedApi.endpoints.getVideosByMediaTypeAndGenreId.matchFulfilled
      ),
      (state, action) => {
        const {
          page,
          results,
          total_pages,
          total_results,
          mediaType,
          itemKey,
        } = action.payload;
        state[mediaType][itemKey].page = page;
        state[mediaType][itemKey].results.push(...results);
        state[mediaType][itemKey].total_pages = total_pages;
        state[mediaType][itemKey].total_results = total_results;
      }
    );
  },
});

export const { setNextPage, initiateItem } = discoverSlice.actions;
export default discoverSlice.reducer;

const extendedApi = tmdbApi.injectEndpoints({
  endpoints: (build) => ({
    getVideosByMediaTypeAndGenreId: build.query<
      PaginatedMovieResult & {
        mediaType: MEDIA_TYPE;
        itemKey: number | string;
      },
      { mediaType: MEDIA_TYPE; genreId: number; page: number }
    >({
      query: ({ mediaType, genreId, page }) => ({
        url: `/discover/${mediaType}`,
        params: { api_key: TMDB_V3_API_KEY, with_genres: genreId, page },
      }),
      transformResponse: (
        response: PaginatedMovieResult,
        _,
        { mediaType, genreId }
      ) => ({
        ...response,
        mediaType,
        itemKey: genreId,
      }),
    }),
    getVideosByMediaTypeAndCustomGenre: build.query<
      PaginatedMovieResult & {
        mediaType: MEDIA_TYPE;
        itemKey: number | string;
      },
      { mediaType: MEDIA_TYPE; apiString: string; page: number }
    >({
      query: ({ mediaType, apiString, page }) => ({
        url: `/${mediaType}/${apiString}`,
        params: { api_key: TMDB_V3_API_KEY, page },
      }),
      transformResponse: (
        response: PaginatedMovieResult,
        _,
        { mediaType, apiString }
      ) => {
        return {
          ...response,
          mediaType,
          itemKey: apiString,
        };
      },
    }),
    getAppendedVideos: build.query<
      MovieDetail,
      { mediaType: MEDIA_TYPE; id: number }
    >({
      query: ({ mediaType, id }) => ({
        url: `/${mediaType}/${id}`,
        params: { api_key: TMDB_V3_API_KEY, append_to_response: "videos" },
      }),
    }),
    getSimilarVideos: build.query<
      PaginatedMovieResult,
      { mediaType: MEDIA_TYPE; id: number }
    >({
      query: ({ mediaType, id }) => ({
        url: `/${mediaType}/${id}/similar`,
        params: { api_key: TMDB_V3_API_KEY },
      }),
    }),
  }),
});

export const {
  useGetVideosByMediaTypeAndGenreIdQuery,
  useLazyGetVideosByMediaTypeAndGenreIdQuery,
  useGetVideosByMediaTypeAndCustomGenreQuery,
  useLazyGetVideosByMediaTypeAndCustomGenreQuery,
  useGetAppendedVideosQuery,
  useLazyGetAppendedVideosQuery,
  useGetSimilarVideosQuery,
  useLazyGetSimilarVideosQuery,
} = extendedApi;


================================================
FILE: src/store/slices/genre.ts
================================================
import { TMDB_V3_API_KEY } from "src/constant";
import { Genre } from "src/types/Genre";
import { tmdbApi } from "./apiSlice";

const extendedApi = tmdbApi.injectEndpoints({
  endpoints: (build) => ({
    getGenres: build.query<Genre[], string>({
      query: (mediaType) => ({
        url: `/genre/${mediaType}/list`,
        params: { api_key: TMDB_V3_API_KEY },
      }),
      transformResponse: (response: { genres: Genre[] }) => {
        return response.genres;
      },
    }),
  }),
});

export const { useGetGenresQuery, endpoints: genreSliceEndpoints  } = extendedApi;


================================================
FILE: src/theme/palette.ts
================================================
import type { PaletteMode } from "@mui/material";

const PRIMARY = {
  light: "#B8B8B8",
  main: "#141414",
  dark: "#0E0A0A",
};
const GREY = {
  100: "#F9FAFB",
  200: "#F4F6F8",
  300: "#DFE3E8",
  400: "#C4CDD5",
  500: "#919EAB",
  600: "#637381",
  700: "#454F5B",
  800: "#212B36",
  900: "#161C24",
};

const COMMON = {
  common: { black: "#000", white: "#fff" },
  primary: { ...PRIMARY, contrastText: "#fff" },
  grey: GREY,
  action: {
    active: GREY[500],
    hoverOpacity: 0.08,
    disabledOpacity: 0.48,
  },
};

const palette = {
  ...COMMON,
  text: { primary: "#fff", secondary: GREY[500], disabled: GREY[600] },
  background: { default: PRIMARY.main, paper: PRIMARY.main },
  mode: "dark" as PaletteMode,
};

export default palette;


================================================
FILE: src/types/Common.ts
================================================
import { Movie } from "src/types/Movie";

export enum MEDIA_TYPE {
  Movie = "movie",
  Tv = "tv",
}

export type Company = {
  description: string;
  headquarters: string;
  homepage: string;
  id: number;
  logo_path: string;
  name: string;
  origin_country: string;
  parent_company: null | object;
};

export type Country = {
  iso_3166_1: string;
  english_name: string;
};

export type Language = {
  iso_639_1: string;
  english_name: string;
  name: string;
};

export type PaginatedResult = {
  page: number;
  total_pages: number;
  total_results: number;
};

export type PaginatedMovieResult = PaginatedResult & { results: Movie[] };


================================================
FILE: src/types/Genre.ts
================================================
export type Genre = {
  id: number;
  name: string;
};

export type CustomGenre = {
  id?: number;
  name: string;
  apiString: string;
};


================================================
FILE: src/types/Movie.ts
================================================
import { Company, Country, Language } from './Common';
import { Genre } from './Genre';

export type Appended_Video = {
  id: string;
  iso_639_1: string;
  iso_3166_1: string;
  key: string;
  name: string;
  official: boolean;
  published_at: string;
  site: string;
  size: number;
  type: string;
};

export type MovieDetail = {
  adult: boolean;
  backdrop_path: string | null;
  belongs_to_collection: null;
  budget: number;
  genres: Genre[];
  homepage: string;
  id: number;
  imdb_id: string;
  original_language: string;
  original_title: string;
  overview: string;
  popularity: number;
  poster_path: string | null;
  production_companies: Company[];
  production_countries: Country[];
  release_date: string;
  revenue: number;
  runtime: number;
  spoken_languages: Language[];
  status: string;
  tagline: string;
  title: string;
  video: boolean;
  videos: { results: Appended_Video[] };
  vote_average: number;
  vote_count: number;
};

export type Movie = {
  poster_path: string | null;
  adult: boolean;
  overview: string;
  release_date: string;
  genre_ids: number[];
  id: number;
  original_title: string;
  original_language: string;
  title: string;
  backdrop_path: string | null;
  popularity: number;
  vote_count: number;
  video: boolean;
  vote_average: number;
};


================================================
FILE: src/utils/common.ts
================================================
export const getRandomNumber = (maxNumber: number) =>
  Math.floor(Math.random() * maxNumber);

export const formatMinuteToReadable = (minutes: number) => {
  const h = Math.floor(minutes / 60);
  const m = minutes - h * 60;

  if (h > 0) {
    return `${h}h ${m}m`;
  } else {
    return `${m}m`;
  }
};

export const formatBytes = (bytes: number, decimals: number = 2) => {
  if (!+bytes) return "0 Bytes";

  const k = 1024;
  const dm = decimals < 0 ? 0 : decimals;
  const sizes = [
    "Bytes",
    "KiB",
    "MiB",
    "GiB",
    "TiB",
    "PiB",
    "EiB",
    "ZiB",
    "YiB",
  ];

  const i = Math.floor(Math.log(bytes) / Math.log(k));

  return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
};

export const formatTime = (current: number) => {
  const h = Math.floor(current / 3600);
  const m = Math.floor((current - h * 3600) / 60);
  const s = Math.floor(current % 60);

  const sString = s < 10 ? "0" + s.toString() : s.toString();
  const mString = m < 10 ? "0" + m.toString() : m.toString();

  if (h > 0) {
    const hString = h < 10 ? "0" + h.toString() : h.toString();
    return `${hString}:${mString}:${sString}`;
  } else {
    return `${mString}:${sString}`;
  }
};


================================================
FILE: src/utils/index.ts
================================================
export { formatMinuteToReadable, getRandomNumber } from "./common";

function buildThresholdList() {
  let thresholds = [];
  const numSteps = 20;

  for (let i = 1.0; i <= numSteps; i++) {
    let ratio = i / numSteps;
    thresholds.push(ratio);
  }

  thresholds.push(0);
  return thresholds;
}

export { buildThresholdList };


================================================
FILE: src/videojs-youtube.d.ts
================================================
declare module "videojs-youtube" {}


================================================
FILE: src/vite-env.d.ts
================================================
/// <reference types="vite/client" />


================================================
FILE: tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "ESNext",
    "useDefineForClassFields": true,
    "lib": ["DOM", "DOM.Iterable", "ESNext"],
    "allowJs": false,
    "skipLibCheck": true,
    "esModuleInterop": false,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "module": "ESNext",
    "moduleResolution": "Node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "baseUrl": "."
  },
  "include": ["src"],
  "references": [{ "path": "./tsconfig.node.json" }]
}


================================================
FILE: tsconfig.node.json
================================================
{
  "compilerOptions": {
    "composite": true,
    "module": "ESNext",
    "moduleResolution": "Node",
    "allowSyntheticDefaultImports": true
  },
  "include": ["vite.config.ts"]
}


================================================
FILE: vercel.json
================================================
{
  "rewrites": [{ "source": "/(.*)", "destination": "/" }]
}


================================================
FILE: vite.config.ts
================================================
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tsconfigPaths from 'vite-tsconfig-paths'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react(), tsconfigPaths()]
})
Download .txt
gitextract_evgpsfy5/

├── .dockerignore
├── .firebaserc
├── .github/
│   └── workflows/
│       ├── firebase-hosting-merge.yml
│       └── firebase-hosting-pull-request.yml
├── .gitignore
├── Dockerfile
├── README.md
├── firebase.json
├── index.html
├── package.json
├── public/
│   ├── index.html
│   ├── manifest.json
│   └── robots.txt
├── src/
│   ├── CustomClassNameSetup.ts
│   ├── components/
│   │   ├── AgeLimitChip.tsx
│   │   ├── DetailModal.tsx
│   │   ├── GenreBreadcrumbs.tsx
│   │   ├── GridPage.tsx
│   │   ├── GridWithInfiniteScroll.tsx
│   │   ├── HeroSection.tsx
│   │   ├── Logo.tsx
│   │   ├── MainLoadingScreen.tsx
│   │   ├── MaturityRate.tsx
│   │   ├── MaxLineTypography.tsx
│   │   ├── MoreInfoButton.tsx
│   │   ├── NetflixIconButton.tsx
│   │   ├── NetflixNavigationLink.tsx
│   │   ├── PlayButton.tsx
│   │   ├── QualityChip.tsx
│   │   ├── SearchBox.tsx
│   │   ├── SimilarVideoCard.tsx
│   │   ├── VideoCardPortal.tsx
│   │   ├── VideoItemWithHover.tsx
│   │   ├── VideoItemWithHoverPure.tsx
│   │   ├── VideoPortalContainer.tsx
│   │   ├── VideoSlider.tsx
│   │   ├── animate/
│   │   │   ├── MotionContainer.tsx
│   │   │   └── variants/
│   │   │       ├── Wrap.ts
│   │   │       ├── fade/
│   │   │       │   ├── FadeIn.ts
│   │   │       │   └── FadeOut.ts
│   │   │       └── zoom/
│   │   │           └── ZoomIn.ts
│   │   ├── layouts/
│   │   │   ├── Footer.tsx
│   │   │   ├── MainHeader.tsx
│   │   │   └── index.ts
│   │   ├── slick-slider/
│   │   │   ├── CustomNavigation.tsx
│   │   │   └── SlickSlider.tsx
│   │   └── watch/
│   │       ├── PlayerControlButton.tsx
│   │       ├── PlayerSeekbar.tsx
│   │       ├── VideoJSPlayer.tsx
│   │       └── VolumeControllers.tsx
│   ├── constant/
│   │   └── index.ts
│   ├── hoc/
│   │   └── withPagination.tsx
│   ├── hooks/
│   │   ├── redux.ts
│   │   ├── useIntersectionObserver.ts
│   │   ├── useOffSetTop.ts
│   │   └── useWindowSize.ts
│   ├── layouts/
│   │   └── MainLayout.tsx
│   ├── lib/
│   │   └── createSafeContext.ts
│   ├── main.tsx
│   ├── pages/
│   │   ├── GenreExplore.tsx
│   │   ├── HomePage.tsx
│   │   └── WatchPage.tsx
│   ├── providers/
│   │   ├── DetailModalProvider.tsx
│   │   └── PortalProvider.tsx
│   ├── routes/
│   │   └── index.tsx
│   ├── store/
│   │   ├── index.ts
│   │   └── slices/
│   │       ├── apiSlice.ts
│   │       ├── configuration.ts
│   │       ├── discover.ts
│   │       └── genre.ts
│   ├── theme/
│   │   └── palette.ts
│   ├── types/
│   │   ├── Common.ts
│   │   ├── Genre.ts
│   │   └── Movie.ts
│   ├── utils/
│   │   ├── common.ts
│   │   └── index.ts
│   ├── videojs-youtube.d.ts
│   └── vite-env.d.ts
├── tsconfig.json
├── tsconfig.node.json
├── vercel.json
└── vite.config.ts
Download .txt
SYMBOL INDEX (94 symbols across 50 files)

FILE: src/components/AgeLimitChip.tsx
  function AgeLimitChip (line 2) | function AgeLimitChip({ sx, ...others }: ChipProps) {

FILE: src/components/DetailModal.tsx
  function DetailModal (line 40) | function DetailModal() {

FILE: src/components/GenreBreadcrumbs.tsx
  type GenreBreadcrumbsProps (line 17) | interface GenreBreadcrumbsProps extends BreadcrumbsProps {
  function GenreBreadcrumbs (line 21) | function GenreBreadcrumbs({

FILE: src/components/GridPage.tsx
  type GridPageProps (line 6) | interface GridPageProps {
  function GridPage (line 10) | function GridPage({ genre, mediaType }: GridPageProps) {

FILE: src/components/GridWithInfiniteScroll.tsx
  type GridWithInfiniteScrollProps (line 11) | interface GridWithInfiniteScrollProps {
  function GridWithInfiniteScroll (line 16) | function GridWithInfiniteScroll({

FILE: src/components/HeroSection.tsx
  type TopTrailerProps (line 24) | interface TopTrailerProps {
  function TopTrailer (line 28) | function TopTrailer({ mediaType }: TopTrailerProps) {

FILE: src/components/Logo.tsx
  function Logo (line 5) | function Logo({ sx }: BoxProps) {

FILE: src/components/MainLoadingScreen.tsx
  function MainLoadingScreen (line 3) | function MainLoadingScreen() {

FILE: src/components/MaturityRate.tsx
  function MaturityRate (line 4) | function MaturityRate({ children }: { children: ReactNode }) {

FILE: src/components/MoreInfoButton.tsx
  function MoreInfoButton (line 4) | function MoreInfoButton({ sx, ...others }: ButtonProps) {

FILE: src/components/NetflixNavigationLink.tsx
  function NetflixNavigationLink (line 7) | function NetflixNavigationLink({

FILE: src/components/PlayButton.tsx
  function PlayButton (line 6) | function PlayButton({ sx, ...others }: ButtonProps) {

FILE: src/components/QualityChip.tsx
  function QualityChip (line 2) | function QualityChip({ sx, ...others }: ChipProps) {

FILE: src/components/SearchBox.tsx
  function SearchBox (line 36) | function SearchBox() {

FILE: src/components/SimilarVideoCard.tsx
  type SimilarVideoCardProps (line 13) | interface SimilarVideoCardProps {
  function SimilarVideoCard (line 17) | function SimilarVideoCard({ video }: SimilarVideoCardProps) {

FILE: src/components/VideoCardPortal.tsx
  type VideoCardModalProps (line 25) | interface VideoCardModalProps {
  function VideoCardModal (line 30) | function VideoCardModal({

FILE: src/components/VideoItemWithHover.tsx
  type VideoItemWithHoverProps (line 6) | interface VideoItemWithHoverProps {
  function VideoItemWithHover (line 10) | function VideoItemWithHover({ video }: VideoItemWithHoverProps) {

FILE: src/components/VideoItemWithHoverPure.tsx
  type VideoItemWithHoverPureType (line 3) | type VideoItemWithHoverPureType = {
  class VideoItemWithHoverPure (line 9) | class VideoItemWithHoverPure extends PureComponent<VideoItemWithHoverPur...
    method render (line 10) | render() {

FILE: src/components/VideoPortalContainer.tsx
  function VideoPortalContainer (line 14) | function VideoPortalContainer() {

FILE: src/components/VideoSlider.tsx
  type SliderRowForGenreProps (line 6) | interface SliderRowForGenreProps {
  function SliderRowForGenre (line 10) | function SliderRowForGenre({

FILE: src/components/animate/MotionContainer.tsx
  type MotionContainerProps (line 5) | interface MotionContainerProps extends BoxProps {
  function MotionContainer (line 10) | function MotionContainer({

FILE: src/components/animate/variants/fade/FadeIn.ts
  constant TRANSITION_ENTER (line 1) | const TRANSITION_ENTER = {
  constant TRANSITION_EXIT (line 5) | const TRANSITION_EXIT = {

FILE: src/components/animate/variants/fade/FadeOut.ts
  constant TRANSITION_ENTER (line 1) | const TRANSITION_ENTER = {
  constant TRANSITION_EXIT (line 5) | const TRANSITION_EXIT = {

FILE: src/components/animate/variants/zoom/ZoomIn.ts
  constant DISTANCE (line 1) | const DISTANCE = 0;
  constant OUT (line 3) | const OUT = { scale: 0, opacity: 0 };
  constant TRANSITION_ENTER (line 5) | const TRANSITION_ENTER = {
  constant TRANSITION_EXIT (line 10) | const TRANSITION_EXIT = {

FILE: src/components/layouts/Footer.tsx
  function Footer (line 6) | function Footer() {

FILE: src/components/slick-slider/CustomNavigation.tsx
  type CustomNaviationProps (line 30) | interface CustomNaviationProps {
  function CustomNavigation (line 39) | function CustomNavigation({

FILE: src/components/slick-slider/SlickSlider.tsx
  type SlideItemProps (line 52) | interface SlideItemProps {
  function SlideItem (line 56) | function SlideItem({ item }: SlideItemProps) {
  type SlickSliderProps (line 64) | interface SlickSliderProps {
  function SlickSlider (line 69) | function SlickSlider({ data, genre }: SlickSliderProps) {

FILE: src/components/watch/PlayerSeekbar.tsx
  function PlayerSeekbar (line 55) | function PlayerSeekbar({

FILE: src/components/watch/VideoJSPlayer.tsx
  function VideoJSPlayer (line 7) | function VideoJSPlayer({

FILE: src/components/watch/VolumeControllers.tsx
  function VolumeControllers (line 37) | function VolumeControllers({

FILE: src/constant/index.ts
  constant API_ENDPOINT_URL (line 3) | const API_ENDPOINT_URL = import.meta.env.VITE_APP_API_ENDPOINT_URL;
  constant TMDB_V3_API_KEY (line 4) | const TMDB_V3_API_KEY = import.meta.env.VITE_APP_TMDB_V3_API_KEY;
  constant MAIN_PATH (line 6) | const MAIN_PATH = {
  constant ARROW_MAX_WIDTH (line 13) | const ARROW_MAX_WIDTH = 60;
  constant COMMON_TITLES (line 14) | const COMMON_TITLES: CustomGenre[] = [
  constant YOUTUBE_URL (line 21) | const YOUTUBE_URL = "https://www.youtube.com/watch?v=";
  constant APP_BAR_HEIGHT (line 22) | const APP_BAR_HEIGHT = 70;
  constant INITIAL_DETAIL_STATE (line 24) | const INITIAL_DETAIL_STATE = {

FILE: src/hoc/withPagination.tsx
  function withPagination (line 12) | function withPagination(

FILE: src/hooks/useIntersectionObserver.ts
  function useIntersectionObserver (line 4) | function useIntersectionObserver(

FILE: src/hooks/useOffSetTop.ts
  function useOffSetTop (line 3) | function useOffSetTop(top: number) {

FILE: src/hooks/useWindowSize.ts
  function useWindowSize (line 3) | function useWindowSize() {

FILE: src/layouts/MainLayout.tsx
  function MainLayout (line 12) | function MainLayout() {

FILE: src/lib/createSafeContext.ts
  function createSafeContext (line 3) | function createSafeContext<TValue extends {} | null>() {

FILE: src/pages/GenreExplore.tsx
  function loader (line 16) | async function loader({ params }: LoaderFunctionArgs) {
  function Component (line 30) | function Component() {

FILE: src/pages/HomePage.tsx
  function loader (line 10) | async function loader() {
  function Component (line 16) | function Component() {

FILE: src/pages/WatchPage.tsx
  function Component (line 24) | function Component() {

FILE: src/providers/DetailModalProvider.tsx
  type DetailType (line 10) | interface DetailType {
  type DetailModalConsumerProps (line 14) | interface DetailModalConsumerProps {
  function DetailModalProvider (line 22) | function DetailModalProvider({

FILE: src/providers/PortalProvider.tsx
  type PortalConsumerProps (line 5) | interface PortalConsumerProps {
  type PortalDataConsumerProps (line 8) | interface PortalDataConsumerProps {
  function PortalProvider (line 19) | function PortalProvider({ children }: { children: ReactNode }) {

FILE: src/store/index.ts
  type RootState (line 14) | type RootState = ReturnType<typeof store.getState>;
  type AppDispatch (line 15) | type AppDispatch = typeof store.dispatch;

FILE: src/store/slices/configuration.ts
  type ConfigurationType (line 4) | type ConfigurationType = {

FILE: src/store/slices/discover.ts
  method extraReducers (line 33) | extraReducers(builder) {

FILE: src/theme/palette.ts
  constant PRIMARY (line 3) | const PRIMARY = {
  constant GREY (line 8) | const GREY = {
  constant COMMON (line 20) | const COMMON = {

FILE: src/types/Common.ts
  type MEDIA_TYPE (line 3) | enum MEDIA_TYPE {
  type Company (line 8) | type Company = {
  type Country (line 19) | type Country = {
  type Language (line 24) | type Language = {
  type PaginatedResult (line 30) | type PaginatedResult = {
  type PaginatedMovieResult (line 36) | type PaginatedMovieResult = PaginatedResult & { results: Movie[] };

FILE: src/types/Genre.ts
  type Genre (line 1) | type Genre = {
  type CustomGenre (line 6) | type CustomGenre = {

FILE: src/types/Movie.ts
  type Appended_Video (line 4) | type Appended_Video = {
  type MovieDetail (line 17) | type MovieDetail = {
  type Movie (line 46) | type Movie = {

FILE: src/utils/index.ts
  function buildThresholdList (line 3) | function buildThresholdList() {
Condensed preview — 82 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (122K chars).
[
  {
    "path": ".dockerignore",
    "chars": 21,
    "preview": ".*\nbuild\nnode_modules"
  },
  {
    "path": ".firebaserc",
    "chars": 56,
    "preview": "{\n  \"projects\": {\n    \"default\": \"websites-f0426\"\n  }\n}\n"
  },
  {
    "path": ".github/workflows/firebase-hosting-merge.yml",
    "chars": 760,
    "preview": "# This file was auto-generated by the Firebase CLI\n# https://github.com/firebase/firebase-tools\n\nname: Deploy to Firebas"
  },
  {
    "path": ".github/workflows/firebase-hosting-pull-request.yml",
    "chars": 623,
    "preview": "# This file was auto-generated by the Firebase CLI\n# https://github.com/firebase/firebase-tools\n\nname: Deploy to Firebas"
  },
  {
    "path": ".gitignore",
    "chars": 338,
    "preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
  },
  {
    "path": "Dockerfile",
    "chars": 414,
    "preview": "FROM node:16.17.0-alpine as builder\nWORKDIR /app\nCOPY ./package.json .\nCOPY ./yarn.lock .\nRUN yarn install\nCOPY . .\nARG "
  },
  {
    "path": "README.md",
    "chars": 4958,
    "preview": "<div align=\"center\">\n  <a href=\"http://netflix-clone-with-tmdb-using-react-mui.vercel.app/\">\n    <img src=\"./public/asse"
  },
  {
    "path": "firebase.json",
    "chars": 234,
    "preview": "{\n  \"hosting\": {\n    \"public\": \"dist\",\n    \"ignore\": [\n      \"firebase.json\",\n      \"**/.*\",\n      \"**/node_modules/**\"\n"
  },
  {
    "path": "index.html",
    "chars": 700,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" href=\"/favicon.ico\" />\n    <"
  },
  {
    "path": "package.json",
    "chars": 941,
    "preview": "{\n  \"name\": \"netflix-clone-using-react-typescript-mui\",\n  \"version\": \"0.1.1\",\n  \"type\": \"module\",\n  \"private\": true,\n  \""
  },
  {
    "path": "public/index.html",
    "chars": 1975,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <link rel=\"icon\" href=\"%PUBLIC_URL%/favicon.i"
  },
  {
    "path": "public/manifest.json",
    "chars": 492,
    "preview": "{\n  \"short_name\": \"React App\",\n  \"name\": \"Create React App Sample\",\n  \"icons\": [\n    {\n      \"src\": \"favicon.ico\",\n     "
  },
  {
    "path": "public/robots.txt",
    "chars": 67,
    "preview": "# https://www.robotstxt.org/robotstxt.html\nUser-agent: *\nDisallow:\n"
  },
  {
    "path": "src/CustomClassNameSetup.ts",
    "chars": 501,
    "preview": "import { unstable_ClassNameGenerator as ClassNameGenerator } from \"@mui/material/className\";\n\nClassNameGenerator.configu"
  },
  {
    "path": "src/components/AgeLimitChip.tsx",
    "chars": 354,
    "preview": "import Chip, { ChipProps } from \"@mui/material/Chip\";\nexport default function AgeLimitChip({ sx, ...others }: ChipProps)"
  },
  {
    "path": "src/components/DetailModal.tsx",
    "chars": 9318,
    "preview": "import { forwardRef, useCallback, useRef, useState } from \"react\";\nimport Box from \"@mui/material/Box\";\nimport Grid from"
  },
  {
    "path": "src/components/GenreBreadcrumbs.tsx",
    "chars": 742,
    "preview": "import Box from \"@mui/material/Box\";\nimport Typography from \"@mui/material/Typography\";\nimport Breadcrumbs, { Breadcrumb"
  },
  {
    "path": "src/components/GridPage.tsx",
    "chars": 493,
    "preview": "import withPagination from \"src/hoc/withPagination\";\nimport { MEDIA_TYPE } from \"src/types/Common\";\nimport { CustomGenre"
  },
  {
    "path": "src/components/GridWithInfiniteScroll.tsx",
    "chars": 1930,
    "preview": "import { useRef, useEffect } from \"react\";\nimport Grid from \"@mui/material/Grid\";\nimport Box from \"@mui/material/Box\";\ni"
  },
  {
    "path": "src/components/HeroSection.tsx",
    "chars": 7501,
    "preview": "import { useEffect, useState, useMemo, useCallback, useRef } from \"react\";\nimport Box from \"@mui/material/Box\";\nimport S"
  },
  {
    "path": "src/components/Logo.tsx",
    "chars": 470,
    "preview": "import Box, { BoxProps } from \"@mui/material/Box\";\nimport { Link as RouterLink } from \"react-router-dom\";\nimport { MAIN_"
  },
  {
    "path": "src/components/MainLoadingScreen.tsx",
    "chars": 513,
    "preview": "import CircularProgress from \"@mui/material/CircularProgress\";\n\nfunction MainLoadingScreen() {\n  return (\n    <div\n     "
  },
  {
    "path": "src/components/MaturityRate.tsx",
    "chars": 482,
    "preview": "import Box from \"@mui/material/Box\";\nimport { ReactNode } from \"react\";\n\nexport default function MaturityRate({ children"
  },
  {
    "path": "src/components/MaxLineTypography.tsx",
    "chars": 603,
    "preview": "import { forwardRef } from \"react\";\nimport Typography, { TypographyProps } from \"@mui/material/Typography\";\n\nconst MaxLi"
  },
  {
    "path": "src/components/MoreInfoButton.tsx",
    "chars": 890,
    "preview": "import Button, { ButtonProps } from \"@mui/material/Button\";\nimport InfoOutlinedIcon from \"@mui/icons-material/InfoOutlin"
  },
  {
    "path": "src/components/NetflixIconButton.tsx",
    "chars": 637,
    "preview": "import { forwardRef } from \"react\";\nimport IconButton, { IconButtonProps } from \"@mui/material/IconButton\";\n\nconst Netfl"
  },
  {
    "path": "src/components/NetflixNavigationLink.tsx",
    "chars": 435,
    "preview": "import {\n  Link as RouterLink,\n  LinkProps as RouterLinkProps,\n} from \"react-router-dom\";\nimport Link, { LinkProps } fro"
  },
  {
    "path": "src/components/PlayButton.tsx",
    "chars": 997,
    "preview": "import Button, { ButtonProps } from \"@mui/material/Button\";\nimport PlayArrowIcon from \"@mui/icons-material/PlayArrow\";\ni"
  },
  {
    "path": "src/components/QualityChip.tsx",
    "chars": 357,
    "preview": "import Chip, { ChipProps } from \"@mui/material/Chip\";\nexport default function QualityChip({ sx, ...others }: ChipProps) "
  },
  {
    "path": "src/components/SearchBox.tsx",
    "chars": 1715,
    "preview": "import { useState, useRef } from \"react\";\nimport { styled } from \"@mui/material/styles\";\nimport InputBase from \"@mui/mat"
  },
  {
    "path": "src/components/SimilarVideoCard.tsx",
    "chars": 2864,
    "preview": "import Stack from \"@mui/material/Stack\";\nimport Card from \"@mui/material/Card\";\nimport CardContent from \"@mui/material/C"
  },
  {
    "path": "src/components/VideoCardPortal.tsx",
    "chars": 4623,
    "preview": "import { useNavigate } from \"react-router-dom\";\nimport Stack from \"@mui/material/Stack\";\nimport Card from \"@mui/material"
  },
  {
    "path": "src/components/VideoItemWithHover.tsx",
    "chars": 930,
    "preview": "import { useEffect, useState, useRef } from \"react\";\nimport { Movie } from \"src/types/Movie\";\nimport { usePortal } from "
  },
  {
    "path": "src/components/VideoItemWithHoverPure.tsx",
    "chars": 1385,
    "preview": "import { PureComponent, ForwardedRef, forwardRef } from \"react\";\n\ntype VideoItemWithHoverPureType = {\n  src: string;\n  i"
  },
  {
    "path": "src/components/VideoPortalContainer.tsx",
    "chars": 2274,
    "preview": "import { useRef } from \"react\";\nimport { motion } from \"framer-motion\";\nimport Portal from \"@mui/material/Portal\";\n\nimpo"
  },
  {
    "path": "src/components/VideoSlider.tsx",
    "chars": 489,
    "preview": "import withPagination from \"src/hoc/withPagination\";\nimport { MEDIA_TYPE } from \"src/types/Common\";\nimport { CustomGenre"
  },
  {
    "path": "src/components/animate/MotionContainer.tsx",
    "chars": 540,
    "preview": "import { motion } from \"framer-motion\";\nimport Box, { BoxProps } from \"@mui/material/Box\";\nimport { varWrapBoth } from \""
  },
  {
    "path": "src/components/animate/variants/Wrap.ts",
    "chars": 377,
    "preview": "export const varWrapEnter = {\n  animate: {\n    transition: { staggerChildren: 0.1 },\n  },\n};\n\nexport const varWrapExit ="
  },
  {
    "path": "src/components/animate/variants/fade/FadeIn.ts",
    "chars": 331,
    "preview": "const TRANSITION_ENTER = {\n  duration: 0.64,\n  ease: [0.43, 0.13, 0.23, 0.96],\n};\nconst TRANSITION_EXIT = {\n  duration: "
  },
  {
    "path": "src/components/animate/variants/fade/FadeOut.ts",
    "chars": 332,
    "preview": "const TRANSITION_ENTER = {\n  duration: 0.64,\n  ease: [0.43, 0.13, 0.23, 0.96],\n};\nconst TRANSITION_EXIT = {\n  duration: "
  },
  {
    "path": "src/components/animate/variants/zoom/ZoomIn.ts",
    "chars": 841,
    "preview": "const DISTANCE = 0;\nconst IN = { scale: 1, opacity: 1 };\nconst OUT = { scale: 0, opacity: 0 };\n\nconst TRANSITION_ENTER ="
  },
  {
    "path": "src/components/layouts/Footer.tsx",
    "chars": 930,
    "preview": "import Box from \"@mui/material/Box\";\nimport Link from \"@mui/material/Link\";\nimport Typography from \"@mui/material/Typogr"
  },
  {
    "path": "src/components/layouts/MainHeader.tsx",
    "chars": 4898,
    "preview": "import * as React from \"react\";\nimport AppBar from \"@mui/material/AppBar\";\nimport Box from \"@mui/material/Box\";\nimport S"
  },
  {
    "path": "src/components/layouts/index.ts",
    "chars": 102,
    "preview": "export { default as MainHeader } from \"./MainHeader\";\r\nexport { default as Footer } from \"./Footer\";\r\n"
  },
  {
    "path": "src/components/slick-slider/CustomNavigation.tsx",
    "chars": 2231,
    "preview": "import { styled } from \"@mui/material/styles\";\nimport Box from \"@mui/material/Box\";\nimport ArrowBackIosNewIcon from \"@mu"
  },
  {
    "path": "src/components/slick-slider/SlickSlider.tsx",
    "chars": 5709,
    "preview": "import { useState, useRef } from \"react\";\nimport Slider, { Settings } from \"react-slick\";\nimport { motion } from \"framer"
  },
  {
    "path": "src/components/watch/PlayerControlButton.tsx",
    "chars": 638,
    "preview": "import { forwardRef } from \"react\";\n\nimport IconButton, { IconButtonProps } from \"@mui/material/IconButton\";\n\nconst Play"
  },
  {
    "path": "src/components/watch/PlayerSeekbar.tsx",
    "chars": 1869,
    "preview": "import Slider from \"@mui/material/Slider\";\nimport { styled } from \"@mui/material/styles\";\n\nimport { formatTime } from \"s"
  },
  {
    "path": "src/components/watch/VideoJSPlayer.tsx",
    "chars": 2793,
    "preview": "import { useEffect, useRef } from \"react\";\nimport Player from \"video.js/dist/types/player\";\nimport videojs from \"video.j"
  },
  {
    "path": "src/components/watch/VolumeControllers.tsx",
    "chars": 1819,
    "preview": "import { Stack } from \"@mui/material\";\nimport Slider from \"@mui/material/Slider\";\nimport { styled } from \"@mui/material/"
  },
  {
    "path": "src/constant/index.ts",
    "chars": 788,
    "preview": "import { CustomGenre } from \"src/types/Genre\";\n\nexport const API_ENDPOINT_URL = import.meta.env.VITE_APP_API_ENDPOINT_UR"
  },
  {
    "path": "src/hoc/withPagination.tsx",
    "chars": 1970,
    "preview": "import { ElementType, useCallback, useEffect } from \"react\";\nimport MainLoadingScreen from \"src/components/MainLoadingSc"
  },
  {
    "path": "src/hooks/redux.ts",
    "chars": 349,
    "preview": "import { TypedUseSelectorHook, useDispatch, useSelector } from \"react-redux\";\nimport type { RootState, AppDispatch } fro"
  },
  {
    "path": "src/hooks/useIntersectionObserver.ts",
    "chars": 809,
    "preview": "import { RefObject, useEffect, useState } from \"react\";\r\n// import { buildThresholdList } from \"src/utils\";\r\n\r\nexport de"
  },
  {
    "path": "src/hooks/useOffSetTop.ts",
    "chars": 510,
    "preview": "import { useState, useEffect, useCallback } from \"react\";\n\nexport default function useOffSetTop(top: number) {\n  const ["
  },
  {
    "path": "src/hooks/useWindowSize.ts",
    "chars": 1071,
    "preview": "import { useState, useEffect } from \"react\";\r\n\r\nexport default function useWindowSize() {\r\n  // Initialize state with un"
  },
  {
    "path": "src/layouts/MainLayout.tsx",
    "chars": 1225,
    "preview": "import { Outlet, useLocation, useNavigation } from \"react-router-dom\";\nimport Box from \"@mui/material/Box\";\n\nimport Deta"
  },
  {
    "path": "src/lib/createSafeContext.ts",
    "chars": 430,
    "preview": "import React from \"react\";\n\nexport default function createSafeContext<TValue extends {} | null>() {\n  const context = Re"
  },
  {
    "path": "src/main.tsx",
    "chars": 1028,
    "preview": "import \"slick-carousel/slick/slick.css\";\nimport \"slick-carousel/slick/slick-theme.css\";\nimport \"./CustomClassNameSetup\";"
  },
  {
    "path": "src/pages/GenreExplore.tsx",
    "chars": 1472,
    "preview": "import {\n  LoaderFunctionArgs,\n  useLoaderData,\n  // useParams\n} from \"react-router-dom\";\nimport { COMMON_TITLES } from "
  },
  {
    "path": "src/pages/HomePage.tsx",
    "chars": 1122,
    "preview": "import Stack from \"@mui/material/Stack\";\nimport { COMMON_TITLES } from \"src/constant\";\nimport HeroSection from \"src/comp"
  },
  {
    "path": "src/pages/WatchPage.tsx",
    "chars": 9049,
    "preview": "import { useState, useRef, useMemo } from \"react\";\r\nimport { useNavigate } from \"react-router-dom\";\r\nimport Player from "
  },
  {
    "path": "src/providers/DetailModalProvider.tsx",
    "chars": 1714,
    "preview": "import { ReactNode, useEffect, useState, useCallback } from \"react\";\nimport { useLocation } from \"react-router-dom\";\n\nim"
  },
  {
    "path": "src/providers/PortalProvider.tsx",
    "chars": 1279,
    "preview": "import { ReactNode, useState, useCallback } from \"react\";\nimport { Movie } from \"src/types/Movie\";\nimport createSafeCont"
  },
  {
    "path": "src/routes/index.tsx",
    "chars": 803,
    "preview": "import { Navigate, createBrowserRouter } from \"react-router-dom\";\nimport { MAIN_PATH } from \"src/constant\";\n\nimport Main"
  },
  {
    "path": "src/store/index.ts",
    "chars": 501,
    "preview": "import { configureStore } from \"@reduxjs/toolkit\";\nimport { tmdbApi } from \"./slices/apiSlice\";\nimport discoverReducer f"
  },
  {
    "path": "src/store/slices/apiSlice.ts",
    "chars": 279,
    "preview": "import { API_ENDPOINT_URL } from \"src/constant\";\nimport { createApi, fetchBaseQuery } from \"@reduxjs/toolkit/query/react"
  },
  {
    "path": "src/store/slices/configuration.ts",
    "chars": 682,
    "preview": "import { TMDB_V3_API_KEY } from \"src/constant\";\nimport { tmdbApi } from \"./apiSlice\";\n\ntype ConfigurationType = {\n  imag"
  },
  {
    "path": "src/store/slices/discover.ts",
    "chars": 3953,
    "preview": "import { TMDB_V3_API_KEY } from \"src/constant\";\nimport { tmdbApi } from \"./apiSlice\";\nimport { MEDIA_TYPE, PaginatedMovi"
  },
  {
    "path": "src/store/slices/genre.ts",
    "chars": 580,
    "preview": "import { TMDB_V3_API_KEY } from \"src/constant\";\nimport { Genre } from \"src/types/Genre\";\nimport { tmdbApi } from \"./apiS"
  },
  {
    "path": "src/theme/palette.ts",
    "chars": 754,
    "preview": "import type { PaletteMode } from \"@mui/material\";\n\nconst PRIMARY = {\n  light: \"#B8B8B8\",\n  main: \"#141414\",\n  dark: \"#0E"
  },
  {
    "path": "src/types/Common.ts",
    "chars": 646,
    "preview": "import { Movie } from \"src/types/Movie\";\n\nexport enum MEDIA_TYPE {\n  Movie = \"movie\",\n  Tv = \"tv\",\n}\n\nexport type Compan"
  },
  {
    "path": "src/types/Genre.ts",
    "chars": 139,
    "preview": "export type Genre = {\n  id: number;\n  name: string;\n};\n\nexport type CustomGenre = {\n  id?: number;\n  name: string;\n  api"
  },
  {
    "path": "src/types/Movie.ts",
    "chars": 1302,
    "preview": "import { Company, Country, Language } from './Common';\nimport { Genre } from './Genre';\n\nexport type Appended_Video = {\n"
  },
  {
    "path": "src/utils/common.ts",
    "chars": 1215,
    "preview": "export const getRandomNumber = (maxNumber: number) =>\n  Math.floor(Math.random() * maxNumber);\n\nexport const formatMinut"
  },
  {
    "path": "src/utils/index.ts",
    "chars": 330,
    "preview": "export { formatMinuteToReadable, getRandomNumber } from \"./common\";\n\nfunction buildThresholdList() {\n  let thresholds = "
  },
  {
    "path": "src/videojs-youtube.d.ts",
    "chars": 37,
    "preview": "declare module \"videojs-youtube\" {}\r\n"
  },
  {
    "path": "src/vite-env.d.ts",
    "chars": 38,
    "preview": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "tsconfig.json",
    "chars": 579,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"DOM\", \"DOM.Iterable\","
  },
  {
    "path": "tsconfig.node.json",
    "chars": 184,
    "preview": "{\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Node\",\n    \"allowSynthe"
  },
  {
    "path": "vercel.json",
    "chars": 65,
    "preview": "{\r\n  \"rewrites\": [{ \"source\": \"/(.*)\", \"destination\": \"/\" }]\r\n}\r\n"
  },
  {
    "path": "vite.config.ts",
    "chars": 227,
    "preview": "import { defineConfig } from 'vite'\nimport react from '@vitejs/plugin-react'\nimport tsconfigPaths from 'vite-tsconfig-pa"
  }
]

About this extraction

This page contains the full source code of the jason-liu22/netflix-clone-react-typescript GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 82 files (108.0 KB), approximately 28.9k tokens, and a symbol index with 94 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!