[
  {
    "path": ".dockerignore",
    "content": ".*\nbuild\nnode_modules"
  },
  {
    "path": ".firebaserc",
    "content": "{\n  \"projects\": {\n    \"default\": \"websites-f0426\"\n  }\n}\n"
  },
  {
    "path": ".github/workflows/firebase-hosting-merge.yml",
    "content": "# This file was auto-generated by the Firebase CLI\n# https://github.com/firebase/firebase-tools\n\nname: Deploy to Firebase Hosting on merge\n'on':\n  push:\n    branches:\n      - main\njobs:\n  build_and_deploy:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      - run: npm ci && npm run build\n        env:\n          VITE_APP_API_ENDPOINT_URL: '${{ secrets.VITE_APP_API_ENDPOINT_URL }}'\n          VITE_APP_TMDB_V3_API_KEY: '${{ secrets.VITE_APP_TMDB_V3_API_KEY }}'\n      - uses: FirebaseExtended/action-hosting-deploy@v0\n        with:\n          repoToken: '${{ secrets.GITHUB_TOKEN }}'\n          firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_WEBSITES_F0426 }}'\n          channelId: live\n          projectId: websites-f0426\n"
  },
  {
    "path": ".github/workflows/firebase-hosting-pull-request.yml",
    "content": "# This file was auto-generated by the Firebase CLI\n# https://github.com/firebase/firebase-tools\n\nname: Deploy to Firebase Hosting on PR\n'on': pull_request\njobs:\n  build_and_preview:\n    if: '${{ github.event.pull_request.head.repo.full_name == github.repository }}'\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      - run: npm ci && npm run build\n      - uses: FirebaseExtended/action-hosting-deploy@v0\n        with:\n          repoToken: '${{ secrets.GITHUB_TOKEN }}'\n          firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_WEBSITES_F0426 }}'\n          projectId: websites-f0426\n"
  },
  {
    "path": ".gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# env\n.env\n\n# testing\n/coverage\n\n# production\n/build\n/dist\n\n# misc\n.DS_Store\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\nnpm-debug.log*\nyarn.lock\nyarn-debug.log*\nyarn-error.log*\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM node:16.17.0-alpine as builder\nWORKDIR /app\nCOPY ./package.json .\nCOPY ./yarn.lock .\nRUN yarn install\nCOPY . .\nARG TMDB_V3_API_KEY\nENV VITE_APP_TMDB_V3_API_KEY=${TMDB_V3_API_KEY}\nENV VITE_APP_API_ENDPOINT_URL=\"https://api.themoviedb.org/3\"\nRUN yarn build\n\nFROM nginx:stable-alpine\nWORKDIR /usr/share/nginx/html\nRUN rm -rf ./*\nCOPY --from=builder /app/dist .\nEXPOSE 80\nENTRYPOINT [\"nginx\", \"-g\", \"daemon off;\"]"
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n  <a href=\"http://netflix-clone-with-tmdb-using-react-mui.vercel.app/\">\n    <img src=\"./public/assets/netflix-logo.png\" alt=\"Logo\" width=\"100\" height=\"32\">\n  </a>\n\n  <h3 align=\"center\">Netflix Clone</h3>\n\n  <p align=\"center\">\n    <a href=\"https://netflix-clone-react-typescript.vercel.app/\">View Demo</a>\n    ·\n    <a href=\"https://github.com/crazy-man22/netflix-clone-react-typescript/issues\">Report Bug</a>\n    ·\n    <a href=\"https://github.com/crazy-man22/netflix-clone-react-typescript/issues\">Request Feature</a>\n  </p>\n</div>\n\n<details>\n  <summary>Table of Contents</summary>\n  <ol>\n    <li>\n      <a href=\"#prerequests\">Prerequests</a>\n    </li>\n    <li>\n      <a href=\"#which-features-this-project-deals-with\">Which features this project deals with</a>\n    </li>\n    <li><a href=\"#third-party-libraries-used-except-for-react-and-rtk\">Third Party libraries used except for React and RTK</a></li>\n    <li>\n      <a href=\"#contact\">Contact</a>\n    </li>\n  </ol>\n</details>\n\n<br />\n\n<div align=\"center\">\n  <img src=\"./public/assets/home-page.png\" alt=\"Logo\" width=\"100%\" height=\"100%\">\n  <p align=\"center\">Home Page</p>\n  <img src=\"./public/assets/mini-portal.png\" alt=\"Logo\" width=\"100%\" height=\"100%\">\n  <p align=\"center\">Mini Portal</p>\n  <img src=\"./public/assets/detail-modal.png\" alt=\"Logo\" width=\"100%\" height=\"100%\">\n  <p align=\"center\">Detail Modal</p>\n  <img src=\"./public/assets/grid-genre.png\" alt=\"Logo\" width=\"100%\" height=\"100%\">\n  <p align=\"center\">Grid Genre Page</p>\n  <img src=\"./public/assets/watch.png\" alt=\"Logo\" width=\"100%\" height=\"100%\">\n  <p align=\"center\">Watch Page with customer contol bar</p>\n</div>\n\n## Prerequests\n\n- Create an account if you don't have on [TMDB](https://www.themoviedb.org/).\n  Because I use its free API to consume movie/tv data.\n- And then follow the [documentation](https://developers.themoviedb.org/3/getting-started/introduction) to create API Key\n- Finally, if you use v3 of TMDB API, create a file named `.env`, and copy and paste the content of `.env.example`.\n  And then paste the API Key you just created.\n\n## Which features this project deal with\n\n- How to create and use [Custom Hooks](https://reactjs.org/docs/hooks-custom.html)\n- How to use [Context](https://reactjs.org/docs/context.html) and its provider\n- How to use lazy and Suspense for [Code-Splitting](https://reactjs.org/docs/code-splitting.html)\n- How to use a new [lazy](https://reactrouter.com/en/main/route/lazy) feature of react-router to reduce bundle size.\n- 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.\n- How to use [Portal](https://reactjs.org/docs/portals.html)\n- How to use [Fowarding Refs](https://reactjs.org/docs/forwarding-refs.html) to make components reusuable\n- How to create and use [HOC](https://reactjs.org/docs/higher-order-components.html)\n- How to customize default theme of [MUI](https://mui.com/)\n- How to use [RTK](https://redux-toolkit.js.org/introduction/getting-started)\n- How to use [RTK Query](https://redux-toolkit.js.org/rtk-query/overview)\n- How to customize default classname of [MUI](https://mui.com/material-ui/experimental-api/classname-generator)\n- Infinite Scrolling(using [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API))\n- How to make awesome carousel using [slick-carousel](https://react-slick.neostack.com)\n\n## Third Party libraries used except for React and RTK\n\n- [react-router-dom@v6.9](https://reactrouter.com/en/main)\n- [MUI(Material UI)](https://mui.com/)\n- [framer-motion](https://www.framer.com/docs/)\n- [video.js](https://videojs.com)\n- [react-slick](https://react-slick.neostack.com/)\n\n## Install with Docker\n\n```sh\ndocker build --build-arg TMDB_V3_API_KEY=your_api_key_here -t netflix-clone .\n\ndocker run --name netflix-clone-website --rm -d -p 80:80 netflix-clone\n```\n\n## Todo\n\n- Make the animation of video card portal more similar to Netflix.\n- 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.\n- 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).\n- Add accessibilities for better UX.\n- Add Tests.\n"
  },
  {
    "path": "firebase.json",
    "content": "{\n  \"hosting\": {\n    \"public\": \"dist\",\n    \"ignore\": [\n      \"firebase.json\",\n      \"**/.*\",\n      \"**/node_modules/**\"\n    ],\n    \"rewrites\": [\n      {\n        \"source\": \"**\",\n        \"destination\": \"/index.html\"\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" href=\"/favicon.ico\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <link rel=\"apple-touch-icon\" href=\"/logo192.png\" />\n    <link rel=\"manifest\" href=\"/manifest.json\" />\n    <link\n      rel=\"stylesheet\"\n      href=\"https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap\"\n    />\n    <link\n      rel=\"stylesheet\"\n      href=\"https://fonts.googleapis.com/icon?family=Material+Icons\"\n    />\n    <title>Netflix</title>\n  </head>\n  <body style=\"margin: 0\">\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"netflix-clone-using-react-typescript-mui\",\n  \"version\": \"0.1.1\",\n  \"type\": \"module\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc && vite build\",\n    \"preview\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"@emotion/react\": \"^11.10.0\",\n    \"@emotion/styled\": \"^11.10.0\",\n    \"@mui/icons-material\": \"^5.8.4\",\n    \"@mui/material\": \"^5.10.0\",\n    \"@reduxjs/toolkit\": \"^1.8.3\",\n    \"framer-motion\": \"^7.1.0\",\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"react-redux\": \"^8.0.2\",\n    \"react-router-dom\": \"^6.9.0\",\n    \"react-slick\": \"^0.29.0\",\n    \"slick-carousel\": \"^1.8.1\",\n    \"video.js\": \"^8.3.0\",\n    \"videojs-youtube\": \"^3.0.1\",\n    \"vite-tsconfig-paths\": \"^3.5.2\"\n  },\n  \"devDependencies\": {\n    \"@types/react\": \"^18.0.24\",\n    \"@types/react-dom\": \"^18.0.8\",\n    \"@types/react-slick\": \"^0.23.10\",\n    \"@vitejs/plugin-react\": \"^2.2.0\",\n    \"typescript\": \"^4.6.4\",\n    \"vite\": \"^3.2.1\"\n  }\n}\n"
  },
  {
    "path": "public/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <link rel=\"icon\" href=\"%PUBLIC_URL%/favicon.ico\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <meta name=\"theme-color\" content=\"#000000\" />\n    <meta\n      name=\"description\"\n      content=\"Web site created using create-react-app\"\n    />\n    <link rel=\"apple-touch-icon\" href=\"%PUBLIC_URL%/logo192.png\" />\n    <!--\n      manifest.json provides metadata used when your web app is installed on a\n      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/\n    -->\n    <link rel=\"manifest\" href=\"%PUBLIC_URL%/manifest.json\" />\n    <link\n      rel=\"stylesheet\"\n      href=\"https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap\"\n    />\n    <link\n      rel=\"stylesheet\"\n      href=\"https://fonts.googleapis.com/icon?family=Material+Icons\"\n    />\n    <!--\n      Notice the use of %PUBLIC_URL% in the tags above.\n      It will be replaced with the URL of the `public` folder during the build.\n      Only files inside the `public` folder can be referenced from the HTML.\n\n      Unlike \"/favicon.ico\" or \"favicon.ico\", \"%PUBLIC_URL%/favicon.ico\" will\n      work correctly both with client-side routing and a non-root public URL.\n      Learn how to configure a non-root public URL by running `npm run build`.\n    -->\n    <title>Netflix</title>\n  </head>\n  <body style=\"margin: 0\">\n    <noscript>You need to enable JavaScript to run this app.</noscript>\n    <div id=\"root\"></div>\n    <!--\n      This HTML file is a template.\n      If you open it directly in the browser, you will see an empty page.\n\n      You can add webfonts, meta tags, or analytics to this file.\n      The build step will place the bundled scripts into the <body> tag.\n\n      To begin the development, run `npm start` or `yarn start`.\n      To create a production bundle, use `npm run build` or `yarn build`.\n    -->\n  </body>\n</html>\n"
  },
  {
    "path": "public/manifest.json",
    "content": "{\n  \"short_name\": \"React App\",\n  \"name\": \"Create React App Sample\",\n  \"icons\": [\n    {\n      \"src\": \"favicon.ico\",\n      \"sizes\": \"64x64 32x32 24x24 16x16\",\n      \"type\": \"image/x-icon\"\n    },\n    {\n      \"src\": \"logo192.png\",\n      \"type\": \"image/png\",\n      \"sizes\": \"192x192\"\n    },\n    {\n      \"src\": \"logo512.png\",\n      \"type\": \"image/png\",\n      \"sizes\": \"512x512\"\n    }\n  ],\n  \"start_url\": \".\",\n  \"display\": \"standalone\",\n  \"theme_color\": \"#000000\",\n  \"background_color\": \"#ffffff\"\n}\n"
  },
  {
    "path": "public/robots.txt",
    "content": "# https://www.robotstxt.org/robotstxt.html\nUser-agent: *\nDisallow:\n"
  },
  {
    "path": "src/CustomClassNameSetup.ts",
    "content": "import { unstable_ClassNameGenerator as ClassNameGenerator } from \"@mui/material/className\";\n\nClassNameGenerator.configure((componentName) => {\n  let newComponentName = componentName;\n  // you can replace Mui, default prefix of every component with new one you want\n  newComponentName = newComponentName.replace(\"Mui\", \"Netflix\");\n  // you can replace default classname of component with new one you want\n  newComponentName = newComponentName.replace(\"Button\", \"Btn\");\n\n  return newComponentName;\n});\n"
  },
  {
    "path": "src/components/AgeLimitChip.tsx",
    "content": "import Chip, { ChipProps } from \"@mui/material/Chip\";\nexport default function AgeLimitChip({ sx, ...others }: ChipProps) {\n  return (\n    <Chip\n      {...others}\n      sx={{\n        borderRadius: 0,\n        p: 0.5,\n        fontSize: 12,\n        height: \"100%\",\n        \"& > span\": { p: 0 },\n        ...sx,\n      }}\n      variant=\"outlined\"\n    />\n  );\n}\n"
  },
  {
    "path": "src/components/DetailModal.tsx",
    "content": "import { forwardRef, useCallback, useRef, useState } from \"react\";\nimport Box from \"@mui/material/Box\";\nimport Grid from \"@mui/material/Grid\";\nimport Container from \"@mui/material/Container\";\nimport Stack from \"@mui/material/Stack\";\nimport IconButton from \"@mui/material/IconButton\";\nimport Typography from \"@mui/material/Typography\";\nimport Dialog from \"@mui/material/Dialog\";\nimport DialogContent from \"@mui/material/DialogContent\";\nimport Slide from \"@mui/material/Slide\";\nimport { TransitionProps } from \"@mui/material/transitions\";\nimport CloseIcon from \"@mui/icons-material/Close\";\nimport AddIcon from \"@mui/icons-material/Add\";\nimport ThumbUpOffAltIcon from \"@mui/icons-material/ThumbUpOffAlt\";\nimport VolumeUpIcon from \"@mui/icons-material/VolumeUp\";\nimport VolumeOffIcon from \"@mui/icons-material/VolumeOff\";\nimport Player from \"video.js/dist/types/player\";\n\nimport MaxLineTypography from \"./MaxLineTypography\";\nimport PlayButton from \"./PlayButton\";\nimport NetflixIconButton from \"./NetflixIconButton\";\nimport AgeLimitChip from \"./AgeLimitChip\";\nimport QualityChip from \"./QualityChip\";\nimport { formatMinuteToReadable, getRandomNumber } from \"src/utils/common\";\nimport SimilarVideoCard from \"./SimilarVideoCard\";\nimport { useDetailModal } from \"src/providers/DetailModalProvider\";\nimport { useGetSimilarVideosQuery } from \"src/store/slices/discover\";\nimport { MEDIA_TYPE } from \"src/types/Common\";\nimport VideoJSPlayer from \"./watch/VideoJSPlayer\";\n\nconst Transition = forwardRef(function Transition(\n  props: TransitionProps & {\n    children: React.ReactElement<any, any>;\n  },\n  ref: React.Ref<unknown>\n) {\n  return <Slide direction=\"up\" ref={ref} {...props} />;\n});\n\nexport default function DetailModal() {\n  const { detail, setDetailType } = useDetailModal();\n  const { data: similarVideos } = useGetSimilarVideosQuery(\n    { mediaType: detail.mediaType ?? MEDIA_TYPE.Movie, id: detail.id ?? 0 },\n    { skip: !detail.id }\n  );\n  const playerRef = useRef<Player | null>(null);\n  const [muted, setMuted] = useState(true);\n\n  const handleReady = useCallback((player: Player) => {\n    playerRef.current = player;\n    setMuted(player.muted());\n  }, []);\n\n  const handleMute = useCallback((status: boolean) => {\n    if (playerRef.current) {\n      playerRef.current.muted(!status);\n      setMuted(!status);\n    }\n  }, []);\n\n  if (detail.mediaDetail) {\n    return (\n      <Dialog\n        fullWidth\n        scroll=\"body\"\n        maxWidth=\"md\"\n        open={!!detail.mediaDetail}\n        id=\"detail_dialog\"\n        TransitionComponent={Transition}\n      >\n        <DialogContent sx={{ p: 0, bgcolor: \"#181818\" }}>\n          <Box\n            sx={{\n              top: 0,\n              left: 0,\n              right: 0,\n              position: \"relative\",\n              mb: 3,\n            }}\n          >\n            <Box\n              sx={{\n                width: \"100%\",\n                position: \"relative\",\n                height: \"calc(9 / 16 * 100%)\",\n              }}\n            >\n              <VideoJSPlayer\n                options={{\n                  loop: true,\n                  autoplay: true,\n                  controls: false,\n                  responsive: true,\n                  fluid: true,\n                  techOrder: [\"youtube\"],\n                  sources: [\n                    {\n                      type: \"video/youtube\",\n                      src: `https://www.youtube.com/watch?v=${\n                        detail.mediaDetail?.videos.results[0]?.key ||\n                        \"L3oOldViIgY\"\n                      }`,\n                    },\n                  ],\n                }}\n                onReady={handleReady}\n              />\n\n              <Box\n                sx={{\n                  background: `linear-gradient(77deg,rgba(0,0,0,.6),transparent 85%)`,\n                  top: 0,\n                  left: 0,\n                  bottom: 0,\n                  right: \"26.09%\",\n                  opacity: 1,\n                  position: \"absolute\",\n                  transition: \"opacity .5s\",\n                }}\n              />\n              <Box\n                sx={{\n                  backgroundColor: \"transparent\",\n                  backgroundImage:\n                    \"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)\",\n                  backgroundRepeat: \"repeat-x\",\n                  backgroundPosition: \"0px top\",\n                  backgroundSize: \"100% 100%\",\n                  bottom: 0,\n                  position: \"absolute\",\n                  height: \"14.7vw\",\n                  opacity: 1,\n                  top: \"auto\",\n                  width: \"100%\",\n                }}\n              />\n              <IconButton\n                onClick={() => {\n                  setDetailType({ mediaType: undefined, id: undefined });\n                }}\n                sx={{\n                  top: 15,\n                  right: 15,\n                  position: \"absolute\",\n                  bgcolor: \"#181818\",\n                  width: { xs: 22, sm: 40 },\n                  height: { xs: 22, sm: 40 },\n                  \"&:hover\": {\n                    bgcolor: \"primary.main\",\n                  },\n                }}\n              >\n                <CloseIcon\n                  sx={{ color: \"white\", fontSize: { xs: 14, sm: 22 } }}\n                />\n              </IconButton>\n              <Box\n                sx={{\n                  position: \"absolute\",\n                  left: 0,\n                  right: 0,\n                  bottom: 16,\n                  px: { xs: 2, sm: 3, md: 5 },\n                }}\n              >\n                <MaxLineTypography variant=\"h4\" maxLine={1} sx={{ mb: 2 }}>\n                  {detail.mediaDetail?.title}\n                </MaxLineTypography>\n                <Stack direction=\"row\" spacing={2} sx={{ mb: 3 }}>\n                  <PlayButton sx={{ color: \"black\", py: 0 }} />\n                  <NetflixIconButton>\n                    <AddIcon />\n                  </NetflixIconButton>\n                  <NetflixIconButton>\n                    <ThumbUpOffAltIcon />\n                  </NetflixIconButton>\n                  <Box flexGrow={1} />\n                  <NetflixIconButton\n                    size=\"large\"\n                    onClick={() => handleMute(muted)}\n                    sx={{ zIndex: 2 }}\n                  >\n                    {!muted ? <VolumeUpIcon /> : <VolumeOffIcon />}\n                  </NetflixIconButton>\n                </Stack>\n\n                <Container\n                  sx={{\n                    p: \"0px !important\",\n                  }}\n                >\n                  <Grid container spacing={5} alignItems=\"center\">\n                    <Grid item xs={12} sm={6} md={8}>\n                      <Stack direction=\"row\" spacing={1} alignItems=\"center\">\n                        <Typography\n                          variant=\"subtitle1\"\n                          sx={{ color: \"success.main\" }}\n                        >{`${getRandomNumber(100)}% Match`}</Typography>\n                        <Typography variant=\"body2\">\n                          {detail.mediaDetail?.release_date.substring(0, 4)}\n                        </Typography>\n                        <AgeLimitChip label={`${getRandomNumber(20)}+`} />\n                        <Typography variant=\"subtitle2\">{`${formatMinuteToReadable(\n                          getRandomNumber(180)\n                        )}`}</Typography>\n                        <QualityChip label=\"HD\" />\n                      </Stack>\n\n                      <MaxLineTypography\n                        maxLine={3}\n                        variant=\"body1\"\n                        sx={{ mt: 2 }}\n                      >\n                        {detail.mediaDetail?.overview}\n                      </MaxLineTypography>\n                    </Grid>\n                    <Grid item xs={12} sm={6} md={4}>\n                      <Typography variant=\"body2\" sx={{ my: 1 }}>\n                        {`Genres : ${detail.mediaDetail?.genres\n                          .map((g) => g.name)\n                          .join(\", \")}`}\n                      </Typography>\n                      <Typography variant=\"body2\" sx={{ my: 1 }}>\n                        {`Available in : ${detail.mediaDetail?.spoken_languages\n                          .map((l) => l.name)\n                          .join(\", \")}`}\n                      </Typography>\n                    </Grid>\n                  </Grid>\n                </Container>\n              </Box>\n            </Box>\n            {similarVideos && similarVideos.results.length > 0 && (\n              <Container\n                sx={{\n                  py: 2,\n                  px: { xs: 2, sm: 3, md: 5 },\n                }}\n              >\n                <Typography variant=\"h6\" sx={{ mb: 2 }}>\n                  More Like This\n                </Typography>\n                <Grid container spacing={2}>\n                  {similarVideos.results.map((sm) => (\n                    <Grid item xs={6} sm={4} key={sm.id}>\n                      <SimilarVideoCard video={sm} />\n                    </Grid>\n                  ))}\n                </Grid>\n              </Container>\n            )}\n          </Box>\n        </DialogContent>\n      </Dialog>\n    );\n  }\n\n  return null;\n}\n"
  },
  {
    "path": "src/components/GenreBreadcrumbs.tsx",
    "content": "import Box from \"@mui/material/Box\";\nimport Typography from \"@mui/material/Typography\";\nimport Breadcrumbs, { BreadcrumbsProps } from \"@mui/material/Breadcrumbs\";\n\nconst Separator = (\n  <Box\n    component=\"span\"\n    sx={{\n      width: 4,\n      height: 4,\n      borderRadius: \"50%\",\n      bgcolor: \"text.disabled\",\n    }}\n  />\n);\n\ninterface GenreBreadcrumbsProps extends BreadcrumbsProps {\n  genres: string[];\n}\n\nexport default function GenreBreadcrumbs({\n  genres,\n  ...others\n}: GenreBreadcrumbsProps) {\n  return (\n    <Breadcrumbs separator={Separator} {...others}>\n      {genres.map((genre, idx) => (\n        <Typography key={idx} sx={{ color: \"text.primary\" }}>\n          {genre}\n        </Typography>\n      ))}\n    </Breadcrumbs>\n  );\n}\n"
  },
  {
    "path": "src/components/GridPage.tsx",
    "content": "import withPagination from \"src/hoc/withPagination\";\nimport { MEDIA_TYPE } from \"src/types/Common\";\nimport { CustomGenre, Genre } from \"src/types/Genre\";\nimport GridWithInfiniteScroll from \"./GridWithInfiniteScroll\";\n\ninterface GridPageProps {\n  genre: Genre | CustomGenre;\n  mediaType: MEDIA_TYPE;\n}\nexport default function GridPage({ genre, mediaType }: GridPageProps) {\n  const Component = withPagination(\n    GridWithInfiniteScroll,\n    mediaType,\n    genre\n  );\n  return <Component />;\n}\n"
  },
  {
    "path": "src/components/GridWithInfiniteScroll.tsx",
    "content": "import { useRef, useEffect } from \"react\";\nimport Grid from \"@mui/material/Grid\";\nimport Box from \"@mui/material/Box\";\nimport Container from \"@mui/material/Container\";\nimport Typography from \"@mui/material/Typography\";\nimport VideoItemWithHover from \"./VideoItemWithHover\";\nimport { CustomGenre, Genre } from \"src/types/Genre\";\nimport { PaginatedMovieResult } from \"src/types/Common\";\nimport useIntersectionObserver from \"src/hooks/useIntersectionObserver\";\n\ninterface GridWithInfiniteScrollProps {\n  genre: Genre | CustomGenre;\n  data: PaginatedMovieResult;\n  handleNext: (page: number) => void;\n}\nexport default function GridWithInfiniteScroll({\n  genre,\n  data,\n  handleNext,\n}: GridWithInfiniteScrollProps) {\n  const intersectionRef = useRef<HTMLDivElement>(null);\n  const intersection = useIntersectionObserver(intersectionRef);\n\n  useEffect(() => {\n    if (\n      intersection &&\n      intersection.intersectionRatio === 1 &&\n      data.page < data.total_pages\n    ) {\n      handleNext(data.page + 1);\n    }\n  }, [intersection]);\n\n  return (\n    <>\n      <Container\n        maxWidth={false}\n        sx={{\n          px: { xs: \"30px\", sm: \"60px\" },\n          pb: 4,\n          pt: \"150px\",\n          bgcolor: \"inherit\",\n        }}\n      >\n        <Typography\n          variant=\"h5\"\n          sx={{ color: \"text.primary\", mb: 2 }}\n        >{`${genre.name} Movies`}</Typography>\n        <Grid container spacing={2}>\n          {data.results\n            .filter((v) => !!v.backdrop_path)\n            .map((video, idx) => (\n              <Grid\n                key={`${video.id}_${idx}`}\n                item\n                xs={6}\n                sm={3}\n                md={2}\n                sx={{ zIndex: 1 }}\n              >\n                <VideoItemWithHover video={video} />\n              </Grid>\n            ))}\n        </Grid>\n      </Container>\n      <Box sx={{ display: \"hidden\" }} ref={intersectionRef} />\n    </>\n  );\n}\n"
  },
  {
    "path": "src/components/HeroSection.tsx",
    "content": "import { useEffect, useState, useMemo, useCallback, useRef } from \"react\";\nimport Box from \"@mui/material/Box\";\nimport Stack from \"@mui/material/Stack\";\nimport VolumeUpIcon from \"@mui/icons-material/VolumeUp\";\nimport VolumeOffIcon from \"@mui/icons-material/VolumeOff\";\nimport Player from \"video.js/dist/types/player\";\n\nimport { getRandomNumber } from \"src/utils/common\";\nimport MaxLineTypography from \"./MaxLineTypography\";\nimport PlayButton from \"./PlayButton\";\nimport MoreInfoButton from \"./MoreInfoButton\";\nimport NetflixIconButton from \"./NetflixIconButton\";\nimport MaturityRate from \"./MaturityRate\";\nimport useOffSetTop from \"src/hooks/useOffSetTop\";\nimport { useDetailModal } from \"src/providers/DetailModalProvider\";\nimport { MEDIA_TYPE } from \"src/types/Common\";\nimport {\n  useGetVideosByMediaTypeAndCustomGenreQuery,\n  useLazyGetAppendedVideosQuery,\n} from \"src/store/slices/discover\";\nimport { Movie } from \"src/types/Movie\";\nimport VideoJSPlayer from \"./watch/VideoJSPlayer\";\n\ninterface TopTrailerProps {\n  mediaType: MEDIA_TYPE;\n}\n\nexport default function TopTrailer({ mediaType }: TopTrailerProps) {\n  const { data } = useGetVideosByMediaTypeAndCustomGenreQuery({\n    mediaType,\n    apiString: \"popular\",\n    page: 1,\n  });\n  const [getVideoDetail, { data: detail }] = useLazyGetAppendedVideosQuery();\n  const [video, setVideo] = useState<Movie | null>(null);\n  const [muted, setMuted] = useState(true);\n  const playerRef = useRef<Player | null>(null);\n  const isOffset = useOffSetTop(window.innerWidth * 0.5625);\n  const { setDetailType } = useDetailModal();\n  const maturityRate = useMemo(() => {\n    return getRandomNumber(20);\n  }, []);\n\n  const handleReady = useCallback((player: Player) => {\n    playerRef.current = player;\n  }, []);\n\n  useEffect(() => {\n    if (playerRef.current) {\n      if (isOffset) {\n        playerRef.current.pause();\n      } else {\n        if (playerRef.current.paused()) {\n          playerRef.current.play();\n        }\n      }\n    }\n  }, [isOffset]);\n\n  useEffect(() => {\n    if (data && data.results) {\n      const videos = data.results.filter((item) => !!item.backdrop_path);\n      setVideo(videos[getRandomNumber(videos.length)]);\n    }\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [data]);\n\n  useEffect(() => {\n    if (video) {\n      getVideoDetail({ mediaType, id: video.id });\n    }\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [video]);\n\n  const handleMute = useCallback((status: boolean) => {\n    if (playerRef.current) {\n      playerRef.current.muted(!status);\n      setMuted(!status);\n    }\n  }, []);\n\n  return (\n    <Box sx={{ position: \"relative\", zIndex: 1 }}>\n      <Box\n        sx={{\n          mb: 3,\n          pb: \"40%\",\n          top: 0,\n          left: 0,\n          right: 0,\n          position: \"relative\",\n        }}\n      >\n        <Box\n          sx={{\n            width: \"100%\",\n            height: \"56.25vw\",\n            position: \"absolute\",\n          }}\n        >\n          {video && (\n            <>\n              <Box\n                sx={{\n                  top: 0,\n                  left: 0,\n                  right: 0,\n                  bottom: 0,\n                  position: \"absolute\",\n                }}\n              >\n                {detail && (\n                  <VideoJSPlayer\n                    options={{\n                      loop: true,\n                      muted: true,\n                      autoplay: true,\n                      controls: false,\n                      responsive: true,\n                      fluid: true,\n                      techOrder: [\"youtube\"],\n                      sources: [\n                        {\n                          type: \"video/youtube\",\n                          src: `https://www.youtube.com/watch?v=${\n                            detail.videos.results[0]?.key || \"L3oOldViIgY\"\n                          }`,\n                        },\n                      ],\n                    }}\n                    onReady={handleReady}\n                  />\n                )}\n                <Box\n                  sx={{\n                    background: `linear-gradient(77deg,rgba(0,0,0,.6),transparent 85%)`,\n                    top: 0,\n                    left: 0,\n                    bottom: 0,\n                    right: \"26.09%\",\n                    opacity: 1,\n                    position: \"absolute\",\n                    transition: \"opacity .5s\",\n                  }}\n                />\n                <Box\n                  sx={{\n                    backgroundColor: \"transparent\",\n                    backgroundImage:\n                      \"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)\",\n                    backgroundRepeat: \"repeat-x\",\n                    backgroundPosition: \"0px top\",\n                    backgroundSize: \"100% 100%\",\n                    bottom: 0,\n                    position: \"absolute\",\n                    height: \"14.7vw\",\n                    opacity: 1,\n                    top: \"auto\",\n                    width: \"100%\",\n                  }}\n                />\n                <Stack\n                  direction=\"row\"\n                  spacing={2}\n                  sx={{\n                    alignItems: \"center\",\n                    position: \"absolute\",\n                    right: 0,\n                    bottom: \"35%\",\n                  }}\n                >\n                  <NetflixIconButton\n                    size=\"large\"\n                    onClick={() => handleMute(muted)}\n                    sx={{ zIndex: 2 }}\n                  >\n                    {!muted ? <VolumeUpIcon /> : <VolumeOffIcon />}\n                  </NetflixIconButton>\n                  <MaturityRate>{`${maturityRate}+`}</MaturityRate>\n                </Stack>\n              </Box>\n\n              <Box\n                sx={{\n                  position: \"absolute\",\n                  top: 0,\n                  left: 0,\n                  right: 0,\n                  bottom: 0,\n                  width: \"100%\",\n                  height: \"100%\",\n                }}\n              >\n                <Stack\n                  spacing={4}\n                  sx={{\n                    bottom: \"35%\",\n                    position: \"absolute\",\n                    left: { xs: \"4%\", md: \"60px\" },\n                    top: 0,\n                    width: \"36%\",\n                    zIndex: 10,\n                    justifyContent: \"flex-end\",\n                  }}\n                >\n                  <MaxLineTypography\n                    variant=\"h2\"\n                    maxLine={1}\n                    color=\"text.primary\"\n                  >\n                    {video.title}\n                  </MaxLineTypography>\n                  <MaxLineTypography\n                    variant=\"h5\"\n                    maxLine={3}\n                    color=\"text.primary\"\n                  >\n                    {video.overview}\n                  </MaxLineTypography>\n                  <Stack direction={{ xs: \"column\", sm: \"row\" }} spacing={2}>\n                    <PlayButton size=\"large\" />\n                    <MoreInfoButton\n                      size=\"large\"\n                      onClick={() => {\n                        setDetailType({ mediaType, id: video.id });\n                      }}\n                    />\n                  </Stack>\n                </Stack>\n              </Box>\n            </>\n          )}\n        </Box>\n      </Box>\n    </Box>\n  );\n}\n"
  },
  {
    "path": "src/components/Logo.tsx",
    "content": "import Box, { BoxProps } from \"@mui/material/Box\";\nimport { Link as RouterLink } from \"react-router-dom\";\nimport { MAIN_PATH } from \"src/constant\";\n\nexport default function Logo({ sx }: BoxProps) {\n  return (\n    <RouterLink to={`/${MAIN_PATH.browse}`}>\n      <Box\n        component=\"img\"\n        alt=\"Netflix Logo\"\n        src=\"/assets/netflix-logo.png\"\n        width={87}\n        height={25}\n        sx={{\n          ...sx,\n        }}\n      />\n    </RouterLink>\n  );\n}\n"
  },
  {
    "path": "src/components/MainLoadingScreen.tsx",
    "content": "import CircularProgress from \"@mui/material/CircularProgress\";\n\nfunction MainLoadingScreen() {\n  return (\n    <div\n      style={{\n        top: 0,\n        left: 0,\n        right: 0,\n        bottom: 0,\n        display: \"flex\",\n        justifyContent: \"center\",\n        alignItems: \"center\",\n        position: \"fixed\",\n        backgroundColor: \"#141414\",\n        opacity: 0.75,\n        zIndex: 2,\n      }}\n    >\n      <CircularProgress sx={{ color: \"white\" }} />\n    </div>\n  );\n}\n\nexport default MainLoadingScreen;\n"
  },
  {
    "path": "src/components/MaturityRate.tsx",
    "content": "import Box from \"@mui/material/Box\";\nimport { ReactNode } from \"react\";\n\nexport default function MaturityRate({ children }: { children: ReactNode }) {\n  return (\n    <Box\n      sx={{\n        py: 1,\n        pl: 1.5,\n        pr: 3,\n        fontSize: 22,\n        display: \"flex\",\n        alignItem: \"center\",\n        color: \"text.primary\",\n        border: \"3px #dcdcdc\",\n        borderLeftStyle: \"solid\",\n        bgcolor: \"#33333399\",\n      }}\n    >\n      {children}\n    </Box>\n  );\n}\n"
  },
  {
    "path": "src/components/MaxLineTypography.tsx",
    "content": "import { forwardRef } from \"react\";\nimport Typography, { TypographyProps } from \"@mui/material/Typography\";\n\nconst MaxLineTypography = forwardRef<\n  HTMLDivElement,\n  TypographyProps & { maxLine: number }\n>(({ maxLine, children, sx, ...others }, ref) => {\n  return (\n    <Typography\n      ref={ref}\n      sx={{\n        overflow: \"hidden\",\n        textOverflow: \"ellipsis\",\n        display: \"-webkit-box\",\n        WebkitLineClamp: maxLine,\n        WebkitBoxOrient: \"vertical\",\n        ...sx,\n      }}\n      {...others}\n    >\n      {children}\n    </Typography>\n  );\n});\n\nexport default MaxLineTypography;\n"
  },
  {
    "path": "src/components/MoreInfoButton.tsx",
    "content": "import Button, { ButtonProps } from \"@mui/material/Button\";\nimport InfoOutlinedIcon from \"@mui/icons-material/InfoOutlined\";\n\nexport default function MoreInfoButton({ sx, ...others }: ButtonProps) {\n  return (\n    <Button\n      variant=\"contained\"\n      startIcon={\n        <InfoOutlinedIcon\n          sx={{\n            fontSize: {\n              xs: \"24px !important\",\n              sm: \"32px !important\",\n              md: \"40px !important\",\n            },\n          }}\n        />\n      }\n      {...others}\n      sx={{\n        ...sx,\n        px: { xs: 1, sm: 2 },\n        py: { xs: 0.5, sm: 1 },\n        fontSize: { xs: 18, sm: 24, md: 28 },\n        lineHeight: 1.5,\n        fontWeight: \"bold\",\n        textTransform: \"capitalize\",\n        bgcolor: \"#6d6d6eb3\",\n        whiteSpace: \"nowrap\",\n        \"&:hover\": { bgcolor: \"#6d6d6e66\" },\n      }}\n    >\n      More Info\n    </Button>\n  );\n}\n"
  },
  {
    "path": "src/components/NetflixIconButton.tsx",
    "content": "import { forwardRef } from \"react\";\nimport IconButton, { IconButtonProps } from \"@mui/material/IconButton\";\n\nconst NetflixIconButton = forwardRef<HTMLButtonElement, IconButtonProps>(\n  ({ children, sx, ...others }, ref) => {\n    return (\n      <IconButton\n        sx={{\n          color: \"white\",\n          borderWidth: \"2px\",\n          borderStyle: \"solid\",\n          borderColor: \"grey.700\",\n          \"&:hover, &:focus\": {\n            borderColor: \"grey.200\",\n          },\n          ...sx,\n        }}\n        {...others}\n        ref={ref}\n      >\n        {children}\n      </IconButton>\n    );\n  }\n);\n\nexport default NetflixIconButton;\n"
  },
  {
    "path": "src/components/NetflixNavigationLink.tsx",
    "content": "import {\n  Link as RouterLink,\n  LinkProps as RouterLinkProps,\n} from \"react-router-dom\";\nimport Link, { LinkProps } from \"@mui/material/Link\";\n\nexport default function NetflixNavigationLink({\n  sx,\n  children,\n  ...others\n}: LinkProps & RouterLinkProps) {\n  return (\n    <Link\n      {...others}\n      component={RouterLink}\n      sx={{ color: \"text.primary\", textDecoration: \"none\", ...sx }}\n    >\n      {children}\n    </Link>\n  );\n}\n"
  },
  {
    "path": "src/components/PlayButton.tsx",
    "content": "import Button, { ButtonProps } from \"@mui/material/Button\";\nimport PlayArrowIcon from \"@mui/icons-material/PlayArrow\";\nimport { useNavigate } from \"react-router-dom\";\nimport { MAIN_PATH } from \"src/constant\";\n\nexport default function PlayButton({ sx, ...others }: ButtonProps) {\n  const navigate = useNavigate();\n  return (\n    <Button\n      color=\"inherit\"\n      variant=\"contained\"\n      startIcon={\n        <PlayArrowIcon\n          sx={{\n            fontSize: {\n              xs: \"24px !important\",\n              sm: \"32px !important\",\n              md: \"40px !important\",\n            },\n          }}\n        />\n      }\n      {...others}\n      sx={{\n        px: { xs: 1, sm: 2 },\n        py: { xs: 0.5, sm: 1 },\n        fontSize: { xs: 18, sm: 24, md: 28 },\n        lineHeight: 1.5,\n        fontWeight: \"bold\",\n        whiteSpace: \"nowrap\",\n        textTransform: \"capitalize\",\n        ...sx,\n      }}\n      onClick={() => navigate(`/${MAIN_PATH.watch}`)}\n    >\n      Play\n    </Button>\n  );\n}\n"
  },
  {
    "path": "src/components/QualityChip.tsx",
    "content": "import Chip, { ChipProps } from \"@mui/material/Chip\";\nexport default function QualityChip({ sx, ...others }: ChipProps) {\n  return (\n    <Chip\n      variant=\"outlined\"\n      {...others}\n      sx={{\n        borderRadius: \"4px\",\n        p: 0.5,\n        fontSize: 12,\n        height: \"100%\",\n        \"& > span\": { p: 0 },\n        ...sx,\n      }}\n    />\n  );\n}\n"
  },
  {
    "path": "src/components/SearchBox.tsx",
    "content": "import { useState, useRef } from \"react\";\nimport { styled } from \"@mui/material/styles\";\nimport InputBase from \"@mui/material/InputBase\";\nimport SearchIcon from \"@mui/icons-material/Search\";\n\nconst Search = styled(\"div\")(({ theme }) => ({\n  position: \"relative\",\n  width: \"100%\",\n  display: \"flex\",\n  alignItems: \"center\",\n}));\n\nconst SearchIconWrapper = styled(\"div\")(({ theme }) => ({\n  cursor: \"pointer\",\n  padding: theme.spacing(0, 1),\n  height: \"100%\",\n  display: \"flex\",\n  alignItems: \"center\",\n  justifyContent: \"center\",\n}));\n\nconst StyledInputBase = styled(InputBase)(({ theme }) => ({\n  color: \"inherit\",\n  \"& .NetflixInputBase-input\": {\n    width: 0,\n    transition: theme.transitions.create(\"width\", {\n      duration: theme.transitions.duration.complex,\n      easing: theme.transitions.easing.easeIn,\n    }),\n    \"&:focus\": {\n      width: \"auto\",\n    },\n  },\n}));\n\nexport default function SearchBox() {\n  const [isFocused, setIsFocused] = useState(false);\n  const searchInputRef = useRef<HTMLInputElement>();\n\n  const handleClickSearchIcon = () => {\n    if (!isFocused) {\n      searchInputRef.current?.focus();\n    }\n  };\n\n  return (\n    <Search\n      sx={\n        isFocused ? { border: \"1px solid white\", backgroundColor: \"black\" } : {}\n      }\n    >\n      <SearchIconWrapper onClick={handleClickSearchIcon}>\n        <SearchIcon />\n      </SearchIconWrapper>\n      <StyledInputBase\n        inputRef={searchInputRef}\n        placeholder=\"Titles, people, genres\"\n        inputProps={{\n          \"aria-label\": \"search\",\n          onFocus: () => {\n            setIsFocused(true);\n          },\n          onBlur: () => {\n            setIsFocused(false);\n          },\n        }}\n      />\n    </Search>\n  );\n}\n"
  },
  {
    "path": "src/components/SimilarVideoCard.tsx",
    "content": "import Stack from \"@mui/material/Stack\";\nimport Card from \"@mui/material/Card\";\nimport CardContent from \"@mui/material/CardContent\";\nimport Typography from \"@mui/material/Typography\";\nimport AddIcon from \"@mui/icons-material/Add\";\nimport { Movie } from \"src/types/Movie\";\nimport NetflixIconButton from \"./NetflixIconButton\";\nimport MaxLineTypography from \"./MaxLineTypography\";\nimport { formatMinuteToReadable, getRandomNumber } from \"src/utils/common\";\nimport AgeLimitChip from \"./AgeLimitChip\";\nimport { useGetConfigurationQuery } from \"src/store/slices/configuration\";\n\ninterface SimilarVideoCardProps {\n  video: Movie;\n}\n\nexport default function SimilarVideoCard({ video }: SimilarVideoCardProps) {\n  const { data: configuration } = useGetConfigurationQuery(undefined);\n\n  return (\n    <Card>\n      <div\n        style={{\n          width: \"100%\",\n          position: \"relative\",\n          paddingTop: \"calc(9 / 16 * 100%)\",\n        }}\n      >\n        <img\n          src={`${configuration?.images.base_url}w780${video.backdrop_path}`}\n          style={{\n            top: 0,\n            height: \"100%\",\n            position: \"absolute\",\n          }}\n        />\n        <div\n          style={{\n            top: 10,\n            right: 15,\n            position: \"absolute\",\n          }}\n        >\n          <Typography variant=\"subtitle2\">{`${formatMinuteToReadable(\n            getRandomNumber(180)\n          )}`}</Typography>\n        </div>\n        <div\n          style={{\n            left: 0,\n            right: 0,\n            bottom: 0,\n            paddingLeft: \"16px\",\n            paddingRight: \"16px\",\n            paddingBottom: \"4px\",\n            position: \"absolute\",\n          }}\n        >\n          <MaxLineTypography\n            maxLine={1}\n            sx={{ width: \"80%\", fontWeight: 700 }}\n            variant=\"subtitle1\"\n          >\n            {video.title}\n          </MaxLineTypography>\n        </div>\n      </div>\n      <CardContent>\n        <Stack spacing={1}>\n          <Stack direction=\"row\" alignItems=\"center\">\n            <div>\n              <Typography\n                variant=\"subtitle2\"\n                sx={{ color: \"success.main\" }}\n              >{`${getRandomNumber(100)}% Match`}</Typography>\n              <Stack direction=\"row\" spacing={1} alignItems=\"center\">\n                <AgeLimitChip label={`${getRandomNumber(20)}+`} />\n                <Typography variant=\"body2\">\n                  {video.release_date.substring(0, 4)}\n                </Typography>\n              </Stack>\n            </div>\n            <div style={{ flexGrow: 1 }} />\n            <NetflixIconButton>\n              <AddIcon />\n            </NetflixIconButton>\n          </Stack>\n          <MaxLineTypography maxLine={4} variant=\"subtitle2\">\n            {video.overview}\n          </MaxLineTypography>\n        </Stack>\n      </CardContent>\n    </Card>\n  );\n}\n"
  },
  {
    "path": "src/components/VideoCardPortal.tsx",
    "content": "import { useNavigate } from \"react-router-dom\";\nimport Stack from \"@mui/material/Stack\";\nimport Card from \"@mui/material/Card\";\nimport CardContent from \"@mui/material/CardContent\";\nimport Typography from \"@mui/material/Typography\";\nimport VolumeUpIcon from \"@mui/icons-material/VolumeUp\";\nimport PlayCircleIcon from \"@mui/icons-material/PlayCircle\";\nimport ThumbUpOffAltIcon from \"@mui/icons-material/ThumbUpOffAlt\";\nimport AddIcon from \"@mui/icons-material/Add\";\nimport ExpandMoreIcon from \"@mui/icons-material/ExpandMore\";\nimport { Movie } from \"src/types/Movie\";\nimport { usePortal } from \"src/providers/PortalProvider\";\nimport { useDetailModal } from \"src/providers/DetailModalProvider\";\nimport { formatMinuteToReadable, getRandomNumber } from \"src/utils/common\";\nimport NetflixIconButton from \"./NetflixIconButton\";\nimport MaxLineTypography from \"./MaxLineTypography\";\nimport AgeLimitChip from \"./AgeLimitChip\";\nimport QualityChip from \"./QualityChip\";\nimport GenreBreadcrumbs from \"./GenreBreadcrumbs\";\nimport { useGetConfigurationQuery } from \"src/store/slices/configuration\";\nimport { MEDIA_TYPE } from \"src/types/Common\";\nimport { useGetGenresQuery } from \"src/store/slices/genre\";\nimport { MAIN_PATH } from \"src/constant\";\n\ninterface VideoCardModalProps {\n  video: Movie;\n  anchorElement: HTMLElement;\n}\n\nexport default function VideoCardModal({\n  video,\n  anchorElement,\n}: VideoCardModalProps) {\n  const navigate = useNavigate();\n\n  const { data: configuration } = useGetConfigurationQuery(undefined);\n  const { data: genres } = useGetGenresQuery(MEDIA_TYPE.Movie);\n  const setPortal = usePortal();\n  const rect = anchorElement.getBoundingClientRect();\n  const { setDetailType } = useDetailModal();\n\n  return (\n    <Card\n      onPointerLeave={() => {\n        setPortal(null, null);\n      }}\n      sx={{\n        width: rect.width * 1.5,\n        height: \"100%\",\n      }}\n    >\n      <div\n        style={{\n          width: \"100%\",\n          position: \"relative\",\n          paddingTop: \"calc(9 / 16 * 100%)\",\n        }}\n      >\n        <img\n          src={`${configuration?.images.base_url}w780${video.backdrop_path}`}\n          style={{\n            top: 0,\n            height: \"100%\",\n            objectFit: \"cover\",\n            position: \"absolute\",\n            backgroundPosition: \"50%\",\n          }}\n        />\n        <div\n          style={{\n            display: \"flex\",\n            flexDirection: \"row\",\n            alignItems: \"center\",\n            left: 0,\n            right: 0,\n            bottom: 0,\n            paddingLeft: \"16px\",\n            paddingRight: \"16px\",\n            paddingBottom: \"4px\",\n            position: \"absolute\",\n          }}\n        >\n          <MaxLineTypography\n            maxLine={2}\n            sx={{ width: \"80%\", fontWeight: 700 }}\n            variant=\"h6\"\n          >\n            {video.title}\n          </MaxLineTypography>\n          <div style={{ flexGrow: 1 }} />\n          <NetflixIconButton>\n            <VolumeUpIcon />\n          </NetflixIconButton>\n        </div>\n      </div>\n      <CardContent>\n        <Stack spacing={1}>\n          <Stack direction=\"row\" spacing={1}>\n            <NetflixIconButton\n              sx={{ p: 0 }}\n              onClick={() => navigate(`/${MAIN_PATH.watch}`)}\n            >\n              <PlayCircleIcon sx={{ width: 40, height: 40 }} />\n            </NetflixIconButton>\n            <NetflixIconButton>\n              <AddIcon />\n            </NetflixIconButton>\n            <NetflixIconButton>\n              <ThumbUpOffAltIcon />\n            </NetflixIconButton>\n            <div style={{ flexGrow: 1 }} />\n            <NetflixIconButton\n              onClick={() => {\n                setDetailType({ mediaType: MEDIA_TYPE.Movie, id: video.id });\n              }}\n            >\n              <ExpandMoreIcon />\n            </NetflixIconButton>\n          </Stack>\n          <Stack direction=\"row\" spacing={1} alignItems=\"center\">\n            <Typography\n              variant=\"subtitle1\"\n              sx={{ color: \"success.main\" }}\n            >{`${getRandomNumber(100)}% Match`}</Typography>\n            <AgeLimitChip label={`${getRandomNumber(20)}+`} />\n            <Typography variant=\"subtitle2\">{`${formatMinuteToReadable(\n              getRandomNumber(180)\n            )}`}</Typography>\n            <QualityChip label=\"HD\" />\n          </Stack>\n          {genres && (\n            <GenreBreadcrumbs\n              genres={genres\n                .filter((genre) => video.genre_ids.includes(genre.id))\n                .map((genre) => genre.name)}\n            />\n          )}\n        </Stack>\n      </CardContent>\n    </Card>\n  );\n}\n"
  },
  {
    "path": "src/components/VideoItemWithHover.tsx",
    "content": "import { useEffect, useState, useRef } from \"react\";\nimport { Movie } from \"src/types/Movie\";\nimport { usePortal } from \"src/providers/PortalProvider\";\nimport { useGetConfigurationQuery } from \"src/store/slices/configuration\";\nimport VideoItemWithHoverPure from \"./VideoItemWithHoverPure\";\ninterface VideoItemWithHoverProps {\n  video: Movie;\n}\n\nexport default function VideoItemWithHover({ video }: VideoItemWithHoverProps) {\n  const setPortal = usePortal();\n  const elementRef = useRef<HTMLDivElement>(null);\n  const [isHovered, setIsHovered] = useState(false);\n\n  const { data: configuration } = useGetConfigurationQuery(undefined);\n\n  useEffect(() => {\n    if (isHovered) {\n      setPortal(elementRef.current, video);\n    }\n  }, [isHovered]);\n\n  return (\n    <VideoItemWithHoverPure\n      ref={elementRef}\n      handleHover={setIsHovered}\n      src={`${configuration?.images.base_url}w300${video.backdrop_path}`}\n    />\n  );\n}\n"
  },
  {
    "path": "src/components/VideoItemWithHoverPure.tsx",
    "content": "import { PureComponent, ForwardedRef, forwardRef } from \"react\";\n\ntype VideoItemWithHoverPureType = {\n  src: string;\n  innerRef: ForwardedRef<HTMLDivElement>;\n  handleHover: (value: boolean) => void;\n};\n\nclass VideoItemWithHoverPure extends PureComponent<VideoItemWithHoverPureType> {\n  render() {\n    return (\n      <div\n        ref={this.props.innerRef}\n        style={{\n          zIndex: 9,\n          cursor: \"pointer\",\n          borderRadius: 0.5,\n          width: \"100%\",\n          position: \"relative\",\n          paddingTop: \"calc(9 / 16 * 100%)\",\n        }}\n      >\n        <img\n          src={this.props.src}\n          style={{\n            top: 0,\n            height: \"100%\",\n            objectFit: \"cover\",\n            position: \"absolute\",\n            borderRadius: \"4px\",\n          }}\n          onPointerEnter={() => {\n            // console.log(\"onPointerEnter\");\n            this.props.handleHover(true);\n          }}\n          onPointerLeave={() => {\n            // console.log(\"onPointerLeave\");\n            this.props.handleHover(false);\n          }}\n        />\n      </div>\n    );\n  }\n}\n\nconst VideoItemWithHoverRef = forwardRef<\n  HTMLDivElement,\n  Omit<VideoItemWithHoverPureType, \"innerRef\">\n>((props, ref) => <VideoItemWithHoverPure {...props} innerRef={ref} />);\nVideoItemWithHoverRef.displayName = \"VideoItemWithHoverRef\";\n\nexport default VideoItemWithHoverRef;\n"
  },
  {
    "path": "src/components/VideoPortalContainer.tsx",
    "content": "import { useRef } from \"react\";\nimport { motion } from \"framer-motion\";\nimport Portal from \"@mui/material/Portal\";\n\nimport VideoCardPortal from \"./VideoCardPortal\";\nimport MotionContainer from \"./animate/MotionContainer\";\nimport {\n  varZoomIn,\n  varZoomInLeft,\n  varZoomInRight,\n} from \"./animate/variants/zoom/ZoomIn\";\nimport { usePortalData } from \"src/providers/PortalProvider\";\n\nexport default function VideoPortalContainer() {\n  const { miniModalMediaData, anchorElement } = usePortalData();\n  const container = useRef(null);\n  const rect = anchorElement?.getBoundingClientRect();\n\n  const hasToRender = !!miniModalMediaData && !!anchorElement;\n  let isFirstElement = false;\n  let isLastElement = false;\n  let variant = varZoomIn;\n  if (hasToRender) {\n    const parentElement = anchorElement.closest(\".slick-active\");\n    const nextSiblingOfParentElement = parentElement?.nextElementSibling;\n    const previousSiblingOfParentElement =\n      parentElement?.previousElementSibling;\n    if (!previousSiblingOfParentElement?.classList.contains(\"slick-active\")) {\n      isFirstElement = true;\n      variant = varZoomInLeft;\n    } else if (\n      !nextSiblingOfParentElement?.classList.contains(\"slick-active\")\n    ) {\n      isLastElement = true;\n      variant = varZoomInRight;\n    }\n  }\n\n  return (\n    <>\n      {hasToRender && (\n        <Portal container={container.current}>\n          <VideoCardPortal\n            video={miniModalMediaData}\n            anchorElement={anchorElement}\n          />\n        </Portal>\n      )}\n      <MotionContainer open={hasToRender} initial=\"initial\">\n        <motion.div\n          ref={container}\n          variants={variant}\n          style={{\n            zIndex: 1,\n            position: \"absolute\",\n            display: \"inline-block\",\n            ...(rect && {\n              top: rect.top + window.pageYOffset - 0.75 * rect.height,\n              ...(isLastElement\n                ? {\n                    right: document.documentElement.clientWidth - rect.right,\n                  }\n                : {\n                    left: isFirstElement\n                      ? rect.left\n                      : rect.left - 0.25 * rect.width,\n                  }),\n            }),\n          }}\n        />\n      </MotionContainer>\n    </>\n  );\n}\n"
  },
  {
    "path": "src/components/VideoSlider.tsx",
    "content": "import withPagination from \"src/hoc/withPagination\";\nimport { MEDIA_TYPE } from \"src/types/Common\";\nimport { CustomGenre, Genre } from \"src/types/Genre\";\nimport SlickSlider from \"./slick-slider/SlickSlider\";\n\ninterface SliderRowForGenreProps {\n  genre: Genre | CustomGenre;\n  mediaType: MEDIA_TYPE;\n}\nexport default function SliderRowForGenre({\n  genre,\n  mediaType,\n}: SliderRowForGenreProps) {\n  const Component = withPagination(SlickSlider, mediaType, genre);\n  return <Component />;\n}\n"
  },
  {
    "path": "src/components/animate/MotionContainer.tsx",
    "content": "import { motion } from \"framer-motion\";\nimport Box, { BoxProps } from \"@mui/material/Box\";\nimport { varWrapBoth } from \"./variants/Wrap\";\n\ninterface MotionContainerProps extends BoxProps {\n  initial?: boolean | string;\n  open?: boolean;\n}\n\nexport default function MotionContainer({\n  open,\n  children,\n  ...other\n}: MotionContainerProps) {\n  return (\n    <Box\n      initial={false}\n      variants={varWrapBoth}\n      component={motion.div}\n      animate={open ? \"animate\" : \"exit\"}\n      {...other}\n    >\n      {children}\n    </Box>\n  );\n}\n"
  },
  {
    "path": "src/components/animate/variants/Wrap.ts",
    "content": "export const varWrapEnter = {\n  animate: {\n    transition: { staggerChildren: 0.1 },\n  },\n};\n\nexport const varWrapExit = {\n  exit: {\n    transition: { staggerChildren: 0.1 },\n  },\n};\n\nexport const varWrapBoth = {\n  animate: {\n    transition: { staggerChildren: 0.07, delayChildren: 0.1 },\n  },\n  exit: {\n    transition: { staggerChildren: 0.05, staggerDirection: -1 },\n  },\n};\n"
  },
  {
    "path": "src/components/animate/variants/fade/FadeIn.ts",
    "content": "const TRANSITION_ENTER = {\n  duration: 0.64,\n  ease: [0.43, 0.13, 0.23, 0.96],\n};\nconst TRANSITION_EXIT = {\n  duration: 0.48,\n  ease: [0.43, 0.13, 0.23, 0.96],\n};\n\nexport const varFadeIn = {\n  initial: { opacity: 0 },\n  animate: { opacity: 1, transition: TRANSITION_ENTER },\n  exit: { opacity: 0, transition: TRANSITION_EXIT },\n};\n"
  },
  {
    "path": "src/components/animate/variants/fade/FadeOut.ts",
    "content": "const TRANSITION_ENTER = {\n  duration: 0.64,\n  ease: [0.43, 0.13, 0.23, 0.96],\n};\nconst TRANSITION_EXIT = {\n  duration: 0.48,\n  ease: [0.43, 0.13, 0.23, 0.96],\n};\n\nexport const varFadeOut = {\n  initial: { opacity: 1 },\n  animate: { opacity: 0, transition: TRANSITION_ENTER },\n  exit: { opacity: 1, transition: TRANSITION_EXIT },\n};\n"
  },
  {
    "path": "src/components/animate/variants/zoom/ZoomIn.ts",
    "content": "const DISTANCE = 0;\nconst IN = { scale: 1, opacity: 1 };\nconst OUT = { scale: 0, opacity: 0 };\n\nconst TRANSITION_ENTER = {\n  duration: 1,\n  ease: [0.43, 0.13, 0.23, 0.96],\n};\n\nconst TRANSITION_EXIT = {\n  duration: 1,\n  ease: [0.43, 0.13, 0.23, 0.96],\n};\n\nexport const varZoomIn = {\n  initial: OUT,\n  animate: { ...IN, transition: TRANSITION_ENTER },\n  exit: { ...OUT, transition: TRANSITION_EXIT },\n};\n\nexport const varZoomInLeft = {\n  initial: { ...OUT, translateX: -DISTANCE },\n  animate: { ...IN, translateX: 0, transition: TRANSITION_ENTER },\n  exit: { ...OUT, translateX: -DISTANCE, transition: TRANSITION_EXIT },\n};\n\nexport const varZoomInRight = {\n  initial: { ...OUT, translateX: DISTANCE },\n  animate: { ...IN, translateX: 0, transition: TRANSITION_ENTER },\n  exit: { ...OUT, translateX: DISTANCE, transition: TRANSITION_EXIT },\n};\n"
  },
  {
    "path": "src/components/layouts/Footer.tsx",
    "content": "import Box from \"@mui/material/Box\";\nimport Link from \"@mui/material/Link\";\nimport Typography from \"@mui/material/Typography\";\nimport Divider from \"@mui/material/Divider\";\n\nexport default function Footer() {\n  return (\n    <Box\n      component=\"footer\"\n      sx={{\n        display: \"flex\",\n        flexDirection: \"column\",\n        justifyContent: \"center\",\n        height: 150,\n        bgcolor: \"inherit\",\n        px: \"60px\",\n      }}\n    >\n      <Divider\n        component=\"div\"\n        sx={{\n          \"::before, ::after\": { top: \"0%\" },\n        }}\n      >\n        <Typography color=\"grey.700\" variant=\"h6\" component=\"span\">\n          Developed by{\" \"}\n          <Link\n            target=\"_blank\"\n            underline=\"none\"\n            sx={{ color: \"text.primary\" }}\n            href=\"https://github.com/crazy-man22\"\n          >\n            Crazy Man\n          </Link>\n        </Typography>\n      </Divider>\n    </Box>\n  );\n}\n"
  },
  {
    "path": "src/components/layouts/MainHeader.tsx",
    "content": "import * as React from \"react\";\nimport AppBar from \"@mui/material/AppBar\";\nimport Box from \"@mui/material/Box\";\nimport Stack from \"@mui/material/Stack\";\nimport Toolbar from \"@mui/material/Toolbar\";\nimport IconButton from \"@mui/material/IconButton\";\nimport Typography from \"@mui/material/Typography\";\nimport Menu from \"@mui/material/Menu\";\nimport MenuIcon from \"@mui/icons-material/Menu\";\nimport Avatar from \"@mui/material/Avatar\";\nimport Tooltip from \"@mui/material/Tooltip\";\nimport MenuItem from \"@mui/material/MenuItem\";\nimport useOffSetTop from \"src/hooks/useOffSetTop\";\nimport { APP_BAR_HEIGHT } from \"src/constant\";\nimport Logo from \"../Logo\";\nimport SearchBox from \"../SearchBox\";\nimport NetflixNavigationLink from \"../NetflixNavigationLink\";\n\nconst pages = [\"My List\", \"Movies\", \"Tv Shows\"];\n\nconst MainHeader = () => {\n  const isOffset = useOffSetTop(APP_BAR_HEIGHT);\n\n  const [anchorElNav, setAnchorElNav] = React.useState<null | HTMLElement>(\n    null\n  );\n  const [anchorElUser, setAnchorElUser] = React.useState<null | HTMLElement>(\n    null\n  );\n\n  const handleOpenNavMenu = (event: React.MouseEvent<HTMLElement>) => {\n    setAnchorElNav(event.currentTarget);\n  };\n  const handleOpenUserMenu = (event: React.MouseEvent<HTMLElement>) => {\n    setAnchorElUser(event.currentTarget);\n  };\n\n  const handleCloseNavMenu = () => {\n    setAnchorElNav(null);\n  };\n\n  const handleCloseUserMenu = () => {\n    setAnchorElUser(null);\n  };\n\n  return (\n    <AppBar\n      sx={{\n        // px: \"4%\",\n        px: \"60px\",\n        height: APP_BAR_HEIGHT,\n        backgroundImage: \"none\",\n        ...(isOffset\n          ? {\n              bgcolor: \"primary.main\",\n              boxShadow: (theme) => theme.shadows[4],\n            }\n          : { boxShadow: 0, bgcolor: \"transparent\" }),\n      }}\n    >\n      <Toolbar disableGutters>\n        <Logo sx={{ mr: { xs: 2, sm: 4 } }} />\n\n        <Box sx={{ flexGrow: 1, display: { xs: \"flex\", md: \"none\" } }}>\n          <IconButton\n            size=\"large\"\n            aria-label=\"account of current user\"\n            aria-controls=\"menu-appbar\"\n            aria-haspopup=\"true\"\n            onClick={handleOpenNavMenu}\n            color=\"inherit\"\n          >\n            <MenuIcon />\n          </IconButton>\n          <Menu\n            id=\"menu-appbar\"\n            anchorEl={anchorElNav}\n            anchorOrigin={{\n              vertical: \"bottom\",\n              horizontal: \"left\",\n            }}\n            keepMounted\n            transformOrigin={{\n              vertical: \"top\",\n              horizontal: \"left\",\n            }}\n            open={Boolean(anchorElNav)}\n            onClose={handleCloseNavMenu}\n            sx={{\n              display: { xs: \"block\", md: \"none\" },\n            }}\n          >\n            {pages.map((page) => (\n              <MenuItem key={page} onClick={handleCloseNavMenu}>\n                <Typography textAlign=\"center\">{page}</Typography>\n              </MenuItem>\n            ))}\n          </Menu>\n        </Box>\n        <Typography\n          variant=\"h5\"\n          noWrap\n          component=\"a\"\n          href=\"\"\n          sx={{\n            mr: 2,\n            display: { xs: \"flex\", md: \"none\" },\n            flexGrow: 1,\n            fontWeight: 700,\n            color: \"inherit\",\n            textDecoration: \"none\",\n          }}\n        >\n          Netflix\n        </Typography>\n        <Stack\n          direction=\"row\"\n          spacing={3}\n          sx={{ flexGrow: 1, display: { xs: \"none\", md: \"flex\" } }}\n        >\n          {pages.map((page) => (\n            <NetflixNavigationLink\n              to=\"\"\n              variant=\"subtitle1\"\n              key={page}\n              onClick={handleCloseNavMenu}\n            >\n              {page}\n            </NetflixNavigationLink>\n          ))}\n        </Stack>\n\n        <Box sx={{ flexGrow: 0, display: \"flex\", gap: 2 }}>\n          <SearchBox />\n          <Tooltip title=\"Open settings\">\n            <IconButton onClick={handleOpenUserMenu} sx={{ p: 0 }}>\n              <Avatar alt=\"user_avatar\" src=\"/avatar.png\" variant=\"rounded\" />\n            </IconButton>\n          </Tooltip>\n          <Menu\n            sx={{ mt: \"45px\" }}\n            id=\"avatar-menu\"\n            anchorEl={anchorElUser}\n            anchorOrigin={{\n              vertical: \"top\",\n              horizontal: \"right\",\n            }}\n            keepMounted\n            transformOrigin={{\n              vertical: \"top\",\n              horizontal: \"right\",\n            }}\n            open={Boolean(anchorElUser)}\n            onClose={handleCloseUserMenu}\n          >\n            {[\"Account\", \"Logout\"].map((setting) => (\n              <MenuItem key={setting} onClick={handleCloseUserMenu}>\n                <Typography textAlign=\"center\">{setting}</Typography>\n              </MenuItem>\n            ))}\n          </Menu>\n        </Box>\n      </Toolbar>\n    </AppBar>\n  );\n};\nexport default MainHeader;\n"
  },
  {
    "path": "src/components/layouts/index.ts",
    "content": "export { default as MainHeader } from \"./MainHeader\";\r\nexport { default as Footer } from \"./Footer\";\r\n"
  },
  {
    "path": "src/components/slick-slider/CustomNavigation.tsx",
    "content": "import { styled } from \"@mui/material/styles\";\nimport Box from \"@mui/material/Box\";\nimport ArrowBackIosNewIcon from \"@mui/icons-material/ArrowBackIosNew\";\nimport ArrowForwardIosIcon from \"@mui/icons-material/ArrowForwardIos\";\nimport { MouseEventHandler, ReactNode } from \"react\";\n\nconst ArrowStyle = styled(Box)(({ theme }) => ({\n  top: 0,\n  bottom: 0,\n  position: \"absolute\",\n  zIndex: 9,\n  height: \"100%\",\n  opacity: 0.48,\n  display: \"flex\",\n  cursor: \"pointer\",\n  alignItems: \"center\",\n  justifyContent: \"center\",\n  color: theme.palette.common.white,\n  // background: theme.palette.grey[700],\n  transition: theme.transitions.create(\"opacity\"),\n  \"&:hover\": {\n    opacity: 0.8,\n    background: theme.palette.grey[900],\n  },\n  [theme.breakpoints.down(\"sm\")]: {\n    display: \"none\",\n  },\n}));\n\ninterface CustomNaviationProps {\n  isEnd: boolean;\n  arrowWidth: number;\n  children: ReactNode;\n  activeSlideIndex: number;\n  onNext: MouseEventHandler<HTMLDivElement>;\n  onPrevious: MouseEventHandler<HTMLDivElement>;\n}\n\nexport default function CustomNavigation({\n  isEnd,\n  onNext,\n  children,\n  onPrevious,\n  arrowWidth,\n  activeSlideIndex,\n}: CustomNaviationProps) {\n  return (\n    <>\n      {activeSlideIndex > 0 && (\n        <ArrowStyle\n          onClick={onPrevious}\n          sx={{\n            left: 0,\n            width: { xs: arrowWidth / 2, sm: arrowWidth },\n            borderTopRightRadius: { xs: \"4px\" },\n            borderBottomRightRadius: { xs: \"4px\" },\n            // backgroundImage: (theme) =>\n            //   `linear-gradient(to right, ${theme.palette.background.default} 0%, rgba(0,0,0,0) 100%)`,\n          }}\n        >\n          <ArrowBackIosNewIcon />\n        </ArrowStyle>\n      )}\n\n      {children}\n      {!isEnd && (\n        <ArrowStyle\n          onClick={onNext}\n          sx={{\n            right: 0,\n            width: { xs: arrowWidth / 2, sm: arrowWidth },\n            borderTopLeftRadius: { xs: \"4px\" },\n            borderBottomLeftRadius: { xs: \"4px\" },\n            // backgroundImage: (theme) =>\n            //   `linear-gradient(to left, ${theme.palette.background.default} 0%, rgba(0,0,0,0) 100%)`,\n          }}\n        >\n          <ArrowForwardIosIcon />\n        </ArrowStyle>\n      )}\n    </>\n  );\n}\n"
  },
  {
    "path": "src/components/slick-slider/SlickSlider.tsx",
    "content": "import { useState, useRef } from \"react\";\nimport Slider, { Settings } from \"react-slick\";\nimport { motion } from \"framer-motion\";\n\nimport { styled, Theme, useTheme } from \"@mui/material/styles\";\nimport Box from \"@mui/material/Box\";\nimport Stack from \"@mui/material/Stack\";\n\nimport CustomNavigation from \"./CustomNavigation\";\nimport VideoItemWithHover from \"src/components/VideoItemWithHover\";\nimport { ARROW_MAX_WIDTH } from \"src/constant\";\nimport NetflixNavigationLink from \"src/components/NetflixNavigationLink\";\nimport MotionContainer from \"src/components/animate/MotionContainer\";\nimport { varFadeIn } from \"src/components/animate/variants/fade/FadeIn\";\nimport { CustomGenre, Genre } from \"src/types/Genre\";\nimport { Movie } from \"src/types/Movie\";\nimport { PaginatedMovieResult } from \"src/types/Common\";\n\nconst RootStyle = styled(\"div\")(() => ({\n  position: \"relative\",\n  overflow: \"inherit\",\n}));\n\nconst StyledSlider = styled(Slider)(\n  ({ theme, padding }: { theme: Theme; padding: number }) => ({\n    display: \"flex !important\",\n    justifyContent: \"center\",\n    overflow: \"initial !important\",\n    \"& > .slick-list\": {\n      overflow: \"visible\",\n    },\n    [theme.breakpoints.up(\"sm\")]: {\n      \"& > .slick-list\": {\n        width: `calc(100% - ${2 * padding}px)`,\n      },\n      \"& .slick-list > .slick-track\": {\n        margin: \"0px !important\",\n      },\n      \"& .slick-list > .slick-track > .slick-current > div > .NetflixBox-root > .NetflixPaper-root:hover\":\n        {\n          transformOrigin: \"0% 50% !important\",\n        },\n    },\n    [theme.breakpoints.down(\"sm\")]: {\n      \"& > .slick-list\": {\n        width: `calc(100% - ${padding}px)`,\n      },\n    },\n  })\n);\n\ninterface SlideItemProps {\n  item: Movie;\n}\n\nfunction SlideItem({ item }: SlideItemProps) {\n  return (\n    <Box sx={{ pr: { xs: 0.5, sm: 1 } }}>\n      <VideoItemWithHover video={item} />\n    </Box>\n  );\n}\n\ninterface SlickSliderProps {\n  data: PaginatedMovieResult;\n  genre: Genre | CustomGenre;\n  handleNext: (page: number) => void;\n}\nexport default function SlickSlider({ data, genre }: SlickSliderProps) {\n  const sliderRef = useRef<Slider>(null);\n  const [activeSlideIndex, setActiveSlideIndex] = useState(0);\n  const [showExplore, setShowExplore] = useState(false);\n  const [isEnd, setIsEnd] = useState(false);\n  const theme = useTheme();\n\n  const beforeChange = async (currentIndex: number, nextIndex: number) => {\n    if (currentIndex < nextIndex) {\n      setActiveSlideIndex(nextIndex);\n    } else if (currentIndex > nextIndex) {\n      setIsEnd(false);\n    }\n    setActiveSlideIndex(nextIndex);\n  };\n\n  const settings: Settings = {\n    speed: 500,\n    arrows: false,\n    infinite: false,\n    lazyLoad: \"ondemand\",\n    slidesToShow: 6,\n    slidesToScroll: 6,\n    // afterChange: (current) => {\n    //   console.log(\"After Change\", current);\n    // },\n    beforeChange,\n    // onEdge: (direction) => {\n    //   console.log(\"Edge: \", direction);\n    // },\n    responsive: [\n      {\n        breakpoint: 1536,\n        settings: {\n          slidesToShow: 5,\n          slidesToScroll: 5,\n        },\n      },\n      {\n        breakpoint: 1200,\n        settings: {\n          slidesToShow: 4,\n          slidesToScroll: 4,\n        },\n      },\n      {\n        breakpoint: 900,\n        settings: {\n          slidesToShow: 3,\n          slidesToScroll: 3,\n        },\n      },\n      {\n        breakpoint: 600,\n        settings: {\n          slidesToShow: 2,\n          slidesToScroll: 2,\n        },\n      },\n    ],\n  };\n\n  const handlePrevious = () => {\n    sliderRef.current?.slickPrev();\n  };\n\n  const handleNext = () => {\n    sliderRef.current?.slickNext();\n  };\n\n  return (\n    <Box sx={{ overflow: \"hidden\", height: \"100%\", zIndex: 1 }}>\n      {data.results.length > 0 && (\n        <>\n          <Stack\n            spacing={2}\n            direction=\"row\"\n            alignItems=\"center\"\n            sx={{ mb: 2, pl: { xs: \"30px\", sm: \"60px\" } }}\n          >\n            <NetflixNavigationLink\n              variant=\"h5\"\n              to={`/genre/${\n                genre.id || genre.name.toLowerCase().replace(\" \", \"_\")\n              }`}\n              sx={{\n                display: \"inline-block\",\n                fontWeight: 700,\n              }}\n              onMouseOver={() => {\n                setShowExplore(true);\n              }}\n              onMouseLeave={() => {\n                setShowExplore(false);\n              }}\n            >\n              {`${genre.name} Movies `}\n              <MotionContainer\n                open={showExplore}\n                initial=\"initial\"\n                sx={{ display: \"inline\", color: \"success.main\" }}\n              >\n                {\"Explore All\".split(\"\").map((letter, index) => (\n                  <motion.span key={index} variants={varFadeIn}>\n                    {letter}\n                  </motion.span>\n                ))}\n              </MotionContainer>\n            </NetflixNavigationLink>\n          </Stack>\n\n          <RootStyle>\n            <CustomNavigation\n              isEnd={isEnd}\n              arrowWidth={ARROW_MAX_WIDTH}\n              onNext={handleNext}\n              onPrevious={handlePrevious}\n              activeSlideIndex={activeSlideIndex}\n            >\n              <StyledSlider\n                ref={sliderRef}\n                {...settings}\n                padding={ARROW_MAX_WIDTH}\n                theme={theme}\n              >\n                {data.results\n                  .filter((i) => !!i.backdrop_path)\n                  .map((item) => (\n                    <SlideItem key={item.id} item={item} />\n                  ))}\n              </StyledSlider>\n            </CustomNavigation>\n          </RootStyle>\n        </>\n      )}\n    </Box>\n  );\n}\n"
  },
  {
    "path": "src/components/watch/PlayerControlButton.tsx",
    "content": "import { forwardRef } from \"react\";\n\nimport IconButton, { IconButtonProps } from \"@mui/material/IconButton\";\n\nconst PlayerControlButton = forwardRef<HTMLButtonElement, IconButtonProps>(\n  ({ children, ...others }, ref) => (\n    <IconButton\n      ref={ref}\n      sx={{\n        padding: { xs: 0.5, sm: 1 },\n        \"& svg, & span\": { transition: \"transform .3s\" },\n        \"&:hover svg, &:hover span\": {\n          msTransform: \"scale(1.3)\",\n          WebkitTransform: \"scale(1.3)\",\n          transform: \"scale(1.3)\",\n        },\n      }}\n      {...others}\n    >\n      {children}\n    </IconButton>\n  )\n);\n\nexport default PlayerControlButton;\n"
  },
  {
    "path": "src/components/watch/PlayerSeekbar.tsx",
    "content": "import Slider from \"@mui/material/Slider\";\nimport { styled } from \"@mui/material/styles\";\n\nimport { formatTime } from \"src/utils/common\";\n\nconst StyledSlider = styled(Slider)({\n  borderRadius: 0,\n  \"& .NetflixSlider-track\": {\n    backgroundColor: \"red !important\",\n    border: 0,\n  },\n  \"& .NetflixSlider-rail\": {\n    border: \"none\",\n    backgroundColor: \"white !important\",\n    opacity: 0.85,\n  },\n  \"& .NetflixSlider-thumb\": {\n    borderRadius: \"50%\",\n    height: 10,\n    width: 10,\n    backgroundColor: \"red\",\n    \"&:focus, &:hover, &.Netflix-active, &.Netflix-focusVisible\": {\n      boxShadow: \"inherit\",\n      height: 15,\n      width: 15,\n    },\n    \"&:before\": {\n      display: \"none\",\n      boxShadow: \"0 2px 2px 0 #fff\",\n      height: 10,\n      width: 10,\n    },\n  },\n  // \"& .NetflixSlider-valueLabel\": {\n  //   lineHeight: 1.2,\n  //   fontSize: 12,\n  //   background: \"unset\",\n  //   padding: 0,\n  //   width: 32,\n  //   height: 32,\n  //   borderRadius: \"50% 50% 50% 0\",\n  //   backgroundColor: \"#52af77\",\n  //   transformOrigin: \"bottom left\",\n  //   transform: \"translate(50%, -100%) rotate(-45deg) scale(0)\",\n  //   \"&:before\": { display: \"none\" },\n  //   \"&.NetflixSlider-valueLabelOpen\": {\n  //     transform: \"translate(50%, -100%) rotate(-45deg) scale(1)\",\n  //   },\n  //   \"& > *\": {\n  //     transform: \"rotate(45deg)\",\n  //   },\n  // },\n});\n\nfunction PlayerSeekbar({\n  playedSeconds,\n  duration,\n  seekTo,\n}: {\n  playedSeconds: number;\n  duration: number;\n  seekTo: (value: number) => void;\n}) {\n  return (\n    <StyledSlider\n      valueLabelDisplay=\"auto\"\n      valueLabelFormat={(v) => formatTime(v)}\n      // components={{\n      //   ValueLabel: ValueLabelComponent,\n      // }}\n      value={playedSeconds}\n      max={duration}\n      onChange={(_, value) => {\n        seekTo(value as number);\n      }}\n    />\n  );\n}\n\nexport default PlayerSeekbar;\n"
  },
  {
    "path": "src/components/watch/VideoJSPlayer.tsx",
    "content": "import { useEffect, useRef } from \"react\";\nimport Player from \"video.js/dist/types/player\";\nimport videojs from \"video.js\";\nimport \"videojs-youtube\";\nimport \"video.js/dist/video-js.css\";\n\nexport default function VideoJSPlayer({\n  options,\n  onReady,\n}: {\n  options: any;\n  onReady: (player: Player) => void;\n}) {\n  const videoRef = useRef<HTMLDivElement | null>(null);\n  const playerRef = useRef<Player | null>(null);\n\n  useEffect(() => {\n    (async function handleVideojs() {\n      // Make sure Video.js player is only initialized once\n      if (!playerRef.current) {\n        // The Video.js player needs to be _inside_ the component el for React 18 Strict Mode.\n        const videoElement = document.createElement(\"video-js\");\n        // videoElement.classList.add(\"vjs-big-play-centered\", \"vjs-16-9\");\n\n        videoRef.current?.appendChild(videoElement);\n        const player = (playerRef.current = videojs(\n          videoElement,\n          options,\n          () => {\n            onReady && onReady(player);\n          }\n        ));\n\n        // import(\"video.js\").then(async ({ default: videojs }) => {\n        //   await import(\"video.js/dist/video-js.css\");\n        //   if (options[\"techOrder\"] && options[\"techOrder\"].includes(\"youtube\")) {\n        //     // eslint-disable-next-line\n        //     await import(\"videojs-youtube\");\n        //   }\n        //   const player = (playerRef.current = videojs(\n        //     videoElement,\n        //     options,\n        //     () => {\n        //       onReady && onReady(player);\n        //     }\n        //   ));\n        // });\n\n        // await import(\"video.js/dist/video-js.css\");\n        // const videojs = await import(\"video.js\");\n        // if (options[\"techOrder\"] && options[\"techOrder\"].includes(\"youtube\")) {\n        //   // eslint-disable-next-line\n        //   await import(\"videojs-youtube\");\n        // }\n        // const player = (playerRef.current = videojs.default(\n        //   videoElement,\n        //   options,\n        //   () => {\n        //     onReady && onReady(player);\n        //   }\n        // ));\n\n        // You could update an existing player in the `else` block here\n        // on prop change, for example:\n      } else {\n        const player = playerRef.current;\n        // player.autoplay(options.autoplay);\n        player.width(options.width);\n        player.height(options.height);\n      }\n    })();\n  }, [options, videoRef]);\n\n  // Dispose the Video.js player when the functional component unmounts\n  useEffect(() => {\n    const player = playerRef.current;\n\n    return () => {\n      if (player && !player.isDisposed()) {\n        player.dispose();\n        playerRef.current = null;\n      }\n    };\n  }, [playerRef]);\n\n  return (\n    <div data-vjs-player>\n      <div ref={videoRef} />\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/watch/VolumeControllers.tsx",
    "content": "import { Stack } from \"@mui/material\";\nimport Slider from \"@mui/material/Slider\";\nimport { styled } from \"@mui/material/styles\";\nimport { SliderUnstyledOwnProps } from \"@mui/base/SliderUnstyled\";\nimport VolumeUpIcon from \"@mui/icons-material/VolumeUp\";\nimport VolumeOffIcon from \"@mui/icons-material/VolumeOff\";\nimport PlayerControlButton from \"./PlayerControlButton\";\n\nconst StyledSlider = styled(Slider)({\n  height: 5,\n  borderRadius: 0,\n  padding: 0,\n  \"& .NetflixSlider-track\": {\n    border: \"none\",\n    backgroundColor: \"red\",\n  },\n  \"& .NetflixSlider-rail\": {\n    border: \"none\",\n    backgroundColor: \"white\",\n    opacity: 0.85,\n  },\n  \"& .NetflixSlider-thumb\": {\n    height: 10,\n    width: 10,\n    backgroundColor: \"red\",\n    \"&:focus, &:hover, &.Netflix-active, &.Netflix-focusVisible\": {\n      boxShadow: \"inherit\",\n      height: 15,\n      width: 15,\n    },\n    \"&:before\": {\n      display: \"none\",\n    },\n  },\n});\n\nexport default function VolumeControllers({\n  value,\n  handleVolume,\n  handleVolumeToggle,\n  muted,\n}: {\n  value: number;\n  handleVolume: SliderUnstyledOwnProps[\"onChange\"];\n  handleVolumeToggle: React.MouseEventHandler<HTMLButtonElement>;\n  muted: boolean;\n}) {\n  return (\n    <Stack\n      direction=\"row\"\n      alignItems=\"center\"\n      spacing={{ xs: 0.5, sm: 1 }}\n      // sx={{\n      //   \"&:hover NetflixSlider-root\": {\n      //     display: \"inline-block\",\n      //   },\n      // }}\n    >\n      <PlayerControlButton onClick={handleVolumeToggle}>\n        {!muted ? <VolumeUpIcon /> : <VolumeOffIcon />}\n      </PlayerControlButton>\n      <StyledSlider\n        max={100}\n        value={value * 100}\n        valueLabelDisplay=\"auto\"\n        valueLabelFormat={(x: number) => x}\n        onChange={handleVolume}\n        sx={{ width: { xs: 60, sm: 80, md: 100 } }}\n      />\n    </Stack>\n  );\n}\n"
  },
  {
    "path": "src/constant/index.ts",
    "content": "import { CustomGenre } from \"src/types/Genre\";\n\nexport const API_ENDPOINT_URL = import.meta.env.VITE_APP_API_ENDPOINT_URL;\nexport const TMDB_V3_API_KEY = import.meta.env.VITE_APP_TMDB_V3_API_KEY;\n\nexport const MAIN_PATH = {\n  root: \"\",\n  browse: \"browse\",\n  genreExplore: \"genre\",\n  watch: \"watch\",\n};\n\nexport const ARROW_MAX_WIDTH = 60;\nexport const COMMON_TITLES: CustomGenre[] = [\n  { name: \"Popular\", apiString: \"popular\" },\n  { name: \"Top Rated\", apiString: \"top_rated\" },\n  { name: \"Now Playing\", apiString: \"now_playing\" },\n  { name: \"Upcoming\", apiString: \"upcoming\" },\n];\n\nexport const YOUTUBE_URL = \"https://www.youtube.com/watch?v=\";\nexport const APP_BAR_HEIGHT = 70;\n\nexport const INITIAL_DETAIL_STATE = {\n  id: undefined,\n  mediaType: undefined,\n  mediaDetail: undefined,\n};\n"
  },
  {
    "path": "src/hoc/withPagination.tsx",
    "content": "import { ElementType, useCallback, useEffect } from \"react\";\nimport MainLoadingScreen from \"src/components/MainLoadingScreen\";\nimport { useAppDispatch, useAppSelector } from \"src/hooks/redux\";\nimport {\n  initiateItem,\n  useLazyGetVideosByMediaTypeAndGenreIdQuery,\n  useLazyGetVideosByMediaTypeAndCustomGenreQuery,\n} from \"src/store/slices/discover\";\nimport { MEDIA_TYPE } from \"src/types/Common\";\nimport { CustomGenre, Genre } from \"src/types/Genre\";\n\nexport default function withPagination(\n  Component: ElementType,\n  mediaType: MEDIA_TYPE,\n  genre: Genre | CustomGenre\n) {\n  return function WithPagination() {\n    const dispatch = useAppDispatch();\n    const itemKey = genre.id ?? (genre as CustomGenre).apiString;\n    const mediaState = useAppSelector((state) => state.discover[mediaType]);\n    const pageState = mediaState ? mediaState[itemKey] : undefined;\n    const [getVideosByMediaTypeAndGenreId] =\n      useLazyGetVideosByMediaTypeAndGenreIdQuery();\n    const [getVideosByMediaTypeAndCustomGenre] =\n      useLazyGetVideosByMediaTypeAndCustomGenreQuery();\n\n    useEffect(() => {\n      if (!mediaState || !pageState) {\n        dispatch(initiateItem({ mediaType, itemKey }));\n      }\n    }, [mediaState, pageState]);\n\n    useEffect(() => {\n      if (pageState && pageState.page === 0) {\n        handleNext(pageState.page + 1);\n      }\n    }, [pageState]);\n\n    const handleNext = useCallback((page: number) => {\n      if (genre.id) {\n        getVideosByMediaTypeAndGenreId({\n          mediaType,\n          genreId: genre.id,\n          page,\n        });\n      } else {\n        getVideosByMediaTypeAndCustomGenre({\n          mediaType,\n          apiString: (genre as CustomGenre).apiString,\n          page,\n        });\n      }\n      // dispatch(setNextPage({ mediaType, itemKey }));\n    }, []);\n\n    if (pageState) {\n      return (\n        <Component genre={genre} data={pageState} handleNext={handleNext} />\n      );\n    }\n    return <MainLoadingScreen />;\n  };\n}\n"
  },
  {
    "path": "src/hooks/redux.ts",
    "content": "import { TypedUseSelectorHook, useDispatch, useSelector } from \"react-redux\";\nimport type { RootState, AppDispatch } from \"../store\";\n\n// Use throughout your app instead of plain `useDispatch` and `useSelector`\nexport const useAppDispatch: () => AppDispatch = useDispatch;\nexport const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;\n"
  },
  {
    "path": "src/hooks/useIntersectionObserver.ts",
    "content": "import { RefObject, useEffect, useState } from \"react\";\r\n// import { buildThresholdList } from \"src/utils\";\r\n\r\nexport default function useIntersectionObserver(\r\n  ref: RefObject<HTMLElement>,\r\n  options?: IntersectionObserverInit\r\n) {\r\n  const [entry, setEntry] = useState<IntersectionObserverEntry | null>(null);\r\n\r\n  useEffect(() => {\r\n    const observer = new IntersectionObserver(\r\n      ([entry]) => {\r\n        setEntry(entry);\r\n      },\r\n      options ?? {\r\n        root: null,\r\n        rootMargin: \"0px\",\r\n        // threshold: buildThresholdList(),\r\n        threshold: 1,\r\n      }\r\n    );\r\n\r\n    if (ref.current) {\r\n      observer.observe(ref.current);\r\n    }\r\n\r\n    return () => {\r\n      if (ref.current) {\r\n        observer.unobserve(ref.current);\r\n      }\r\n    };\r\n  }, []);\r\n\r\n  return entry;\r\n}\r\n"
  },
  {
    "path": "src/hooks/useOffSetTop.ts",
    "content": "import { useState, useEffect, useCallback } from \"react\";\n\nexport default function useOffSetTop(top: number) {\n  const [offsetTop, setOffSetTop] = useState(false);\n  const onScroll = useCallback(() => {\n    if (window.pageYOffset > top) {\n      setOffSetTop(true);\n    } else {\n      setOffSetTop(false);\n    }\n  }, [top]);\n\n  useEffect(() => {\n    window.addEventListener(\"scroll\", onScroll);\n    return () => {\n      window.removeEventListener(\"scroll\", onScroll);\n    };\n  }, [top]);\n\n  return offsetTop;\n}\n"
  },
  {
    "path": "src/hooks/useWindowSize.ts",
    "content": "import { useState, useEffect } from \"react\";\r\n\r\nexport default function useWindowSize() {\r\n  // Initialize state with undefined width/height so server and client renders match\r\n  // Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/\r\n  const [windowSize, setWindowSize] = useState<{\r\n    width: number | undefined;\r\n    height: number | undefined;\r\n  }>({\r\n    width: undefined,\r\n    height: undefined,\r\n  });\r\n\r\n  useEffect(() => {\r\n    // Handler to call on window resize\r\n    function handleResize() {\r\n      // Set window width/height to state\r\n      setWindowSize({\r\n        width: window.innerWidth,\r\n        height: window.innerHeight,\r\n      });\r\n    }\r\n    // Add event listener\r\n    window.addEventListener(\"resize\", handleResize);\r\n    // Call handler right away so state gets updated with initial window size\r\n    handleResize();\r\n    // Remove event listener on cleanup\r\n    return () => window.removeEventListener(\"resize\", handleResize);\r\n  }, []); // Empty array ensures that effect is only run on mount\r\n\r\n  return windowSize;\r\n}\r\n"
  },
  {
    "path": "src/layouts/MainLayout.tsx",
    "content": "import { Outlet, useLocation, useNavigation } from \"react-router-dom\";\nimport Box from \"@mui/material/Box\";\n\nimport DetailModal from \"src/components/DetailModal\";\nimport VideoPortalContainer from \"src/components/VideoPortalContainer\";\nimport DetailModalProvider from \"src/providers/DetailModalProvider\";\nimport PortalProvider from \"src/providers/PortalProvider\";\nimport { MAIN_PATH } from \"src/constant\";\nimport { Footer, MainHeader } from \"src/components/layouts\";\nimport MainLoadingScreen from \"src/components/MainLoadingScreen\";\n\nexport default function MainLayout() {\n  const location = useLocation();\n  const navigation = useNavigation();\n  // console.log(\"Nav Stat: \", navigation.state);\n  return (\n    <Box\n      sx={{\n        width: \"100%\",\n        minHeight: \"100vh\",\n        bgcolor: \"background.default\",\n      }}\n    >\n      <MainHeader />\n      {navigation.state !== \"idle\" && <MainLoadingScreen />}\n      <DetailModalProvider>\n        <DetailModal />\n        <PortalProvider>\n          {/* <MainLoadingScreen /> */}\n          <Outlet />\n          <VideoPortalContainer />\n        </PortalProvider>\n      </DetailModalProvider>\n      {location.pathname !== `/${MAIN_PATH.watch}` && <Footer />}\n    </Box>\n  );\n}\n"
  },
  {
    "path": "src/lib/createSafeContext.ts",
    "content": "import React from \"react\";\n\nexport default function createSafeContext<TValue extends {} | null>() {\n  const context = React.createContext<TValue | undefined>(undefined);\n\n  function useContext() {\n    const value = React.useContext(context);\n    if (value === undefined) {\n      throw new Error(\"useContext must be inside a Provider with a value\");\n    }\n    return value;\n  }\n\n  return [useContext, context.Provider] as const;\n}\n"
  },
  {
    "path": "src/main.tsx",
    "content": "import \"slick-carousel/slick/slick.css\";\nimport \"slick-carousel/slick/slick-theme.css\";\nimport \"./CustomClassNameSetup\";\nimport React from \"react\";\nimport ReactDOM from \"react-dom/client\";\nimport { Provider } from \"react-redux\";\nimport { RouterProvider } from \"react-router-dom\";\nimport { createTheme, ThemeProvider } from \"@mui/material/styles\";\n\nimport store from \"./store\";\nimport { extendedApi } from \"./store/slices/configuration\";\nimport palette from \"./theme/palette\";\nimport router from \"./routes\";\nimport MainLoadingScreen from \"./components/MainLoadingScreen\";\n\nstore.dispatch(extendedApi.endpoints.getConfiguration.initiate(undefined));\n\nconst root = ReactDOM.createRoot(\n  document.getElementById(\"root\") as HTMLElement\n);\nroot.render(\n  <Provider store={store}>\n    <React.StrictMode>\n      <ThemeProvider theme={createTheme({ palette })}>\n        <RouterProvider\n          router={router}\n          fallbackElement={<MainLoadingScreen />}\n        />\n      </ThemeProvider>\n    </React.StrictMode>\n  </Provider>\n);\n"
  },
  {
    "path": "src/pages/GenreExplore.tsx",
    "content": "import {\n  LoaderFunctionArgs,\n  useLoaderData,\n  // useParams\n} from \"react-router-dom\";\nimport { COMMON_TITLES } from \"src/constant\";\nimport GridPage from \"src/components/GridPage\";\nimport { MEDIA_TYPE } from \"src/types/Common\";\nimport { CustomGenre, Genre } from \"src/types/Genre\";\nimport {\n  genreSliceEndpoints,\n  // useGetGenresQuery\n} from \"src/store/slices/genre\";\nimport store from \"src/store\";\n\nexport async function loader({ params }: LoaderFunctionArgs) {\n  let genre: CustomGenre | Genre | undefined = COMMON_TITLES.find(\n    (t) => t.apiString === (params.genreId as string)\n  );\n  if (!genre) {\n    const genres = await store\n      .dispatch(genreSliceEndpoints.getGenres.initiate(MEDIA_TYPE.Movie))\n      .unwrap();\n    genre = genres?.find((t) => t.id.toString() === (params.genreId as string));\n  }\n\n  return genre;\n}\n\nexport function Component() {\n  const genre: CustomGenre | Genre | undefined = useLoaderData() as\n    | CustomGenre\n    | Genre\n    | undefined;\n  // const { genreId } = useParams();\n  // const { data: genres } = useGetGenresQuery(MEDIA_TYPE.Movie);\n  // let genre: Genre | CustomGenre | undefined;\n  // if (isNaN(parseInt(genreId!))) {\n  //   genre = COMMON_TITLES.find((t) => t.apiString === genreId);\n  // } else {\n  //   genre = genres?.find((t) => t.id.toString() === genreId);\n  // }\n  if (genre) {\n    return <GridPage mediaType={MEDIA_TYPE.Movie} genre={genre} />;\n  }\n  return null;\n}\n\nComponent.displayName = \"GenreExplore\";\n"
  },
  {
    "path": "src/pages/HomePage.tsx",
    "content": "import Stack from \"@mui/material/Stack\";\nimport { COMMON_TITLES } from \"src/constant\";\nimport HeroSection from \"src/components/HeroSection\";\nimport { genreSliceEndpoints, useGetGenresQuery } from \"src/store/slices/genre\";\nimport { MEDIA_TYPE } from \"src/types/Common\";\nimport { CustomGenre, Genre } from \"src/types/Genre\";\nimport SliderRowForGenre from \"src/components/VideoSlider\";\nimport store from \"src/store\";\n\nexport async function loader() {\n  await store.dispatch(\n    genreSliceEndpoints.getGenres.initiate(MEDIA_TYPE.Movie)\n  );\n  return null;\n}\nexport function Component() {\n  const { data: genres, isSuccess } = useGetGenresQuery(MEDIA_TYPE.Movie);\n\n  if (isSuccess && genres && genres.length > 0) {\n    return (\n      <Stack spacing={2}>\n        <HeroSection mediaType={MEDIA_TYPE.Movie} />\n        {[...COMMON_TITLES, ...genres].map((genre: Genre | CustomGenre) => (\n          <SliderRowForGenre\n            key={genre.id || genre.name}\n            genre={genre}\n            mediaType={MEDIA_TYPE.Movie}\n          />\n        ))}\n      </Stack>\n    );\n  }\n  return null;\n}\n\nComponent.displayName = \"HomePage\";\n"
  },
  {
    "path": "src/pages/WatchPage.tsx",
    "content": "import { useState, useRef, useMemo } from \"react\";\r\nimport { useNavigate } from \"react-router-dom\";\r\nimport Player from \"video.js/dist/types/player\";\r\nimport { Box, Stack, Typography } from \"@mui/material\";\r\nimport { SliderUnstyledOwnProps } from \"@mui/base/SliderUnstyled\";\r\nimport PlayArrowIcon from \"@mui/icons-material/PlayArrow\";\r\nimport PauseIcon from \"@mui/icons-material/Pause\";\r\nimport SkipNextIcon from \"@mui/icons-material/SkipNext\";\r\nimport FullscreenIcon from \"@mui/icons-material/Fullscreen\";\r\nimport SettingsIcon from \"@mui/icons-material/Settings\";\r\nimport BrandingWatermarkOutlinedIcon from \"@mui/icons-material/BrandingWatermarkOutlined\";\r\nimport KeyboardBackspaceIcon from \"@mui/icons-material/KeyboardBackspace\";\r\n\r\nimport useWindowSize from \"src/hooks/useWindowSize\";\r\nimport { formatTime } from \"src/utils/common\";\r\n\r\nimport MaxLineTypography from \"src/components/MaxLineTypography\";\r\nimport VolumeControllers from \"src/components/watch/VolumeControllers\";\r\nimport VideoJSPlayer from \"src/components/watch/VideoJSPlayer\";\r\nimport PlayerSeekbar from \"src/components/watch/PlayerSeekbar\";\r\nimport PlayerControlButton from \"src/components/watch/PlayerControlButton\";\r\nimport MainLoadingScreen from \"src/components/MainLoadingScreen\";\r\n\r\nexport function Component() {\r\n  const playerRef = useRef<Player | null>(null);\r\n  const [playerState, setPlayerState] = useState({\r\n    paused: false,\r\n    muted: false,\r\n    playedSeconds: 0,\r\n    duration: 0,\r\n    volume: 0.8,\r\n    loaded: 0,\r\n  });\r\n\r\n  const navigate = useNavigate();\r\n  const [playerInitialized, setPlayerInitialized] = useState(false);\r\n\r\n  const windowSize = useWindowSize();\r\n  const videoJsOptions = useMemo(() => {\r\n    return {\r\n      preload: \"metadata\",\r\n      autoplay: true,\r\n      controls: false,\r\n      // responsive: true,\r\n      // fluid: true,\r\n      width: windowSize.width,\r\n      height: windowSize.height,\r\n      sources: [\r\n        {\r\n          // src: videoData?.video,\r\n          // src: \"https://d2zihajmogu5jn.cloudfront.net/bipbop-advanced/bipbop_16x9_variant.m3u8\",\r\n          src: \"https://bitmovin-a.akamaihd.net/content/sintel/hls/playlist.m3u8\",\r\n          type: \"application/x-mpegurl\",\r\n        },\r\n      ],\r\n    };\r\n    // eslint-disable-next-line react-hooks/exhaustive-deps\r\n  }, [windowSize]);\r\n\r\n  const handlePlayerReady = function (player: Player): void {\r\n    player.on(\"pause\", () => {\r\n      setPlayerState((draft) => {\r\n        return { ...draft, paused: true };\r\n      });\r\n    });\r\n\r\n    player.on(\"play\", () => {\r\n      setPlayerState((draft) => {\r\n        return { ...draft, paused: false };\r\n      });\r\n    });\r\n\r\n    player.on(\"timeupdate\", () => {\r\n      setPlayerState((draft) => {\r\n        return { ...draft, playedSeconds: player.currentTime() };\r\n      });\r\n    });\r\n\r\n    player.one(\"durationchange\", () => {\r\n      setPlayerInitialized(true);\r\n      setPlayerState((draft) => ({ ...draft, duration: player.duration() }));\r\n    });\r\n\r\n    playerRef.current = player;\r\n\r\n    setPlayerState((draft) => {\r\n      return { ...draft, paused: player.paused() };\r\n    });\r\n  };\r\n\r\n  const handleVolumeChange: SliderUnstyledOwnProps[\"onChange\"] = (_, value) => {\r\n    playerRef.current?.volume((value as number) / 100);\r\n    setPlayerState((draft) => {\r\n      return { ...draft, volume: (value as number) / 100 };\r\n    });\r\n  };\r\n\r\n  const handleSeekTo = (v: number) => {\r\n    playerRef.current?.currentTime(v);\r\n  };\r\n\r\n  const handleGoBack = () => {\r\n    navigate(\"/browse\");\r\n  };\r\n\r\n  if (!!videoJsOptions.width) {\r\n    return (\r\n      <Box\r\n        sx={{\r\n          position: \"relative\",\r\n        }}\r\n      >\r\n        <VideoJSPlayer options={videoJsOptions} onReady={handlePlayerReady} />\r\n        {playerRef.current && playerInitialized && (\r\n          <Box\r\n            sx={{\r\n              top: 0,\r\n              left: 0,\r\n              right: 0,\r\n              bottom: 0,\r\n              position: \"absolute\",\r\n            }}\r\n          >\r\n            <Box px={2} sx={{ position: \"absolute\", top: 75 }}>\r\n              <PlayerControlButton onClick={handleGoBack}>\r\n                <KeyboardBackspaceIcon />\r\n              </PlayerControlButton>\r\n            </Box>\r\n            <Box\r\n              px={2}\r\n              sx={{\r\n                position: \"absolute\",\r\n                top: { xs: \"40%\", sm: \"55%\", md: \"60%\" },\r\n                left: 0,\r\n              }}\r\n            >\r\n              <Typography\r\n                variant=\"h3\"\r\n                sx={{\r\n                  fontWeight: 700,\r\n                  color: \"white\",\r\n                }}\r\n              >\r\n                Title\r\n              </Typography>\r\n            </Box>\r\n            <Box\r\n              px={{ xs: 0, sm: 1, md: 2 }}\r\n              sx={{\r\n                position: \"absolute\",\r\n                top: { xs: \"50%\", sm: \"60%\", md: \"70%\" },\r\n                right: 0,\r\n              }}\r\n            >\r\n              <Typography\r\n                variant=\"subtitle2\"\r\n                sx={{\r\n                  px: 1,\r\n                  py: 0.5,\r\n                  fontWeight: 700,\r\n                  color: \"white\",\r\n                  bgcolor: \"red\",\r\n                  borderRadius: \"12px 0px 0px 12px\",\r\n                }}\r\n              >\r\n                12+\r\n              </Typography>\r\n            </Box>\r\n\r\n            <Box\r\n              px={{ xs: 1, sm: 2 }}\r\n              sx={{ position: \"absolute\", bottom: 20, left: 0, right: 0 }}\r\n            >\r\n              {/* Seekbar */}\r\n              <Stack direction=\"row\" alignItems=\"center\" spacing={1}>\r\n                <PlayerSeekbar\r\n                  playedSeconds={playerState.playedSeconds}\r\n                  duration={playerState.duration}\r\n                  seekTo={handleSeekTo}\r\n                />\r\n              </Stack>\r\n              {/* end Seekbar */}\r\n\r\n              {/* Controller */}\r\n              <Stack direction=\"row\" alignItems=\"center\">\r\n                {/* left controller */}\r\n                <Stack\r\n                  direction=\"row\"\r\n                  spacing={{ xs: 0.5, sm: 1.5, md: 2 }}\r\n                  alignItems=\"center\"\r\n                >\r\n                  {!playerState.paused ? (\r\n                    <PlayerControlButton\r\n                      onClick={() => {\r\n                        playerRef.current?.pause();\r\n                      }}\r\n                    >\r\n                      <PauseIcon />\r\n                    </PlayerControlButton>\r\n                  ) : (\r\n                    <PlayerControlButton\r\n                      onClick={() => {\r\n                        playerRef.current?.play();\r\n                      }}\r\n                    >\r\n                      <PlayArrowIcon />\r\n                    </PlayerControlButton>\r\n                  )}\r\n                  <PlayerControlButton>\r\n                    <SkipNextIcon />\r\n                  </PlayerControlButton>\r\n                  <VolumeControllers\r\n                    muted={playerState.muted}\r\n                    handleVolumeToggle={() => {\r\n                      playerRef.current?.muted(!playerState.muted);\r\n                      setPlayerState((draft) => {\r\n                        return { ...draft, muted: !draft.muted };\r\n                      });\r\n                    }}\r\n                    value={playerState.volume}\r\n                    handleVolume={handleVolumeChange}\r\n                  />\r\n                  <Typography variant=\"caption\" sx={{ color: \"white\" }}>\r\n                    {`${formatTime(playerState.playedSeconds)} / ${formatTime(\r\n                      playerState.duration\r\n                    )}`}\r\n                  </Typography>\r\n                </Stack>\r\n                {/* end left controller */}\r\n\r\n                {/* middle time */}\r\n                <Box flexGrow={1}>\r\n                  <MaxLineTypography\r\n                    maxLine={1}\r\n                    variant=\"subtitle1\"\r\n                    textAlign=\"center\"\r\n                    sx={{ maxWidth: 300, mx: \"auto\", color: \"white\" }}\r\n                  >\r\n                    Description\r\n                  </MaxLineTypography>\r\n                </Box>\r\n                {/* end middle time */}\r\n\r\n                {/* right controller */}\r\n                <Stack\r\n                  direction=\"row\"\r\n                  alignItems=\"center\"\r\n                  spacing={{ xs: 0.5, sm: 1.5, md: 2 }}\r\n                >\r\n                  <PlayerControlButton>\r\n                    <SettingsIcon />\r\n                  </PlayerControlButton>\r\n                  <PlayerControlButton>\r\n                    <BrandingWatermarkOutlinedIcon />\r\n                  </PlayerControlButton>\r\n                  <PlayerControlButton>\r\n                    <FullscreenIcon />\r\n                  </PlayerControlButton>\r\n                </Stack>\r\n                {/* end right controller */}\r\n              </Stack>\r\n              {/* end Controller */}\r\n            </Box>\r\n          </Box>\r\n        )}\r\n      </Box>\r\n    );\r\n  }\r\n  return null;\r\n}\r\n\r\nComponent.displayName = \"WatchPage\";\r\n"
  },
  {
    "path": "src/providers/DetailModalProvider.tsx",
    "content": "import { ReactNode, useEffect, useState, useCallback } from \"react\";\nimport { useLocation } from \"react-router-dom\";\n\nimport { INITIAL_DETAIL_STATE } from \"src/constant\";\nimport createSafeContext from \"src/lib/createSafeContext\";\nimport { useLazyGetAppendedVideosQuery } from \"src/store/slices/discover\";\nimport { MEDIA_TYPE } from \"src/types/Common\";\nimport { MovieDetail } from \"src/types/Movie\";\n\ninterface DetailType {\n  id?: number;\n  mediaType?: MEDIA_TYPE;\n}\nexport interface DetailModalConsumerProps {\n  detail: { mediaDetail?: MovieDetail } & DetailType;\n  setDetailType: (newDetailType: DetailType) => void;\n}\n\nexport const [useDetailModal, Provider] =\n  createSafeContext<DetailModalConsumerProps>();\n\nexport default function DetailModalProvider({\n  children,\n}: {\n  children: ReactNode;\n}) {\n  const location = useLocation();\n  const [detail, setDetail] = useState<\n    { mediaDetail?: MovieDetail } & DetailType\n  >(INITIAL_DETAIL_STATE);\n\n  const [getAppendedVideos] = useLazyGetAppendedVideosQuery();\n\n  const handleChangeDetail = useCallback(\n    async (newDetailType: { mediaType?: MEDIA_TYPE; id?: number }) => {\n      if (!!newDetailType.id && newDetailType.mediaType) {\n        const response = await getAppendedVideos({\n          mediaType: newDetailType.mediaType,\n          id: newDetailType.id as number,\n        }).unwrap();\n        setDetail({ ...newDetailType, mediaDetail: response });\n      } else {\n        setDetail(INITIAL_DETAIL_STATE);\n      }\n    },\n    []\n  );\n\n  useEffect(() => {\n    setDetail(INITIAL_DETAIL_STATE);\n  }, [location.pathname, setDetail]);\n\n  return (\n    <Provider value={{ detail, setDetailType: handleChangeDetail }}>\n      {children}\n    </Provider>\n  );\n}\n"
  },
  {
    "path": "src/providers/PortalProvider.tsx",
    "content": "import { ReactNode, useState, useCallback } from \"react\";\nimport { Movie } from \"src/types/Movie\";\nimport createSafeContext from \"src/lib/createSafeContext\";\n\nexport interface PortalConsumerProps {\n  setPortal: (anchor: HTMLElement | null, vidoe: Movie | null) => void;\n}\nexport interface PortalDataConsumerProps {\n  anchorElement: HTMLElement | null;\n  miniModalMediaData: Movie | null;\n}\n\nexport const [usePortal, Provider] =\n  createSafeContext<PortalConsumerProps[\"setPortal\"]>();\n\nexport const [usePortalData, PortalDataProvider] =\n  createSafeContext<PortalDataConsumerProps>();\n\nexport default function PortalProvider({ children }: { children: ReactNode }) {\n  const [anchorElement, setAnchorElement] = useState<HTMLElement | null>(null);\n  const [miniModalMediaData, setMiniModalMediaData] = useState<Movie | null>(\n    null\n  );\n\n  const handleChangePortal = useCallback(\n    (anchor: HTMLElement | null, video: Movie | null) => {\n      setAnchorElement(anchor);\n      setMiniModalMediaData(video);\n    },\n    []\n  );\n\n  return (\n    <Provider\n      value={handleChangePortal}\n    >\n      <PortalDataProvider\n        value={{\n          anchorElement,\n          miniModalMediaData,\n        }}\n      >\n        {children}\n      </PortalDataProvider>\n    </Provider>\n  );\n}\n"
  },
  {
    "path": "src/routes/index.tsx",
    "content": "import { Navigate, createBrowserRouter } from \"react-router-dom\";\nimport { MAIN_PATH } from \"src/constant\";\n\nimport MainLayout from \"src/layouts/MainLayout\";\n\nconst router = createBrowserRouter([\n  {\n    path: \"/\",\n    element: <MainLayout />,\n    children: [\n      {\n        path: MAIN_PATH.root,\n        element: <Navigate to={`/${MAIN_PATH.browse}`} />,\n      },\n      {\n        path: MAIN_PATH.browse,\n        lazy: () => import(\"src/pages/HomePage\"),\n      },\n      {\n        path: MAIN_PATH.genreExplore,\n        children: [\n          {\n            path: \":genreId\",\n            lazy: () => import(\"src/pages/GenreExplore\"),\n          },\n        ],\n      },\n      {\n        path: MAIN_PATH.watch,\n        lazy: () => import(\"src/pages/WatchPage\"),\n      },\n    ],\n  },\n]);\n\nexport default router;\n"
  },
  {
    "path": "src/store/index.ts",
    "content": "import { configureStore } from \"@reduxjs/toolkit\";\nimport { tmdbApi } from \"./slices/apiSlice\";\nimport discoverReducer from \"./slices/discover\";\n\nconst store = configureStore({\n  reducer: {\n    discover: discoverReducer,\n    [tmdbApi.reducerPath]: tmdbApi.reducer,\n  },\n  middleware: (getDefaultMiddleware) =>\n    getDefaultMiddleware().concat(tmdbApi.middleware),\n});\n\nexport type RootState = ReturnType<typeof store.getState>;\nexport type AppDispatch = typeof store.dispatch;\n\nexport default store;\n"
  },
  {
    "path": "src/store/slices/apiSlice.ts",
    "content": "import { API_ENDPOINT_URL } from \"src/constant\";\nimport { createApi, fetchBaseQuery } from \"@reduxjs/toolkit/query/react\";\n\nexport const tmdbApi = createApi({\n  reducerPath: \"tmdbApi\",\n  baseQuery: fetchBaseQuery({ baseUrl: API_ENDPOINT_URL }),\n  endpoints: (build) => ({}),\n});\n"
  },
  {
    "path": "src/store/slices/configuration.ts",
    "content": "import { TMDB_V3_API_KEY } from \"src/constant\";\nimport { tmdbApi } from \"./apiSlice\";\n\ntype ConfigurationType = {\n  images: {\n    base_url: string;\n    secure_base_url: string;\n    backdrop_sizes: string[];\n    logo_sizes: string[];\n    poster_sizes: string[];\n    profile_sizes: string[];\n    still_sizes: string[];\n  };\n  change_keys: string[];\n};\n\nexport const extendedApi = tmdbApi.injectEndpoints({\n  endpoints: (build) => ({\n    getConfiguration: build.query<ConfigurationType, undefined>({\n      query: () => ({\n        url: \"/configuration\",\n        params: { api_key: TMDB_V3_API_KEY },\n      }),\n    }),\n  }),\n});\n\nexport const { useGetConfigurationQuery } = extendedApi;\n"
  },
  {
    "path": "src/store/slices/discover.ts",
    "content": "import { TMDB_V3_API_KEY } from \"src/constant\";\nimport { tmdbApi } from \"./apiSlice\";\nimport { MEDIA_TYPE, PaginatedMovieResult } from \"src/types/Common\";\nimport { MovieDetail } from \"src/types/Movie\";\nimport { createSlice, isAnyOf } from \"@reduxjs/toolkit\";\n\nconst initialState: Record<string, Record<string, PaginatedMovieResult>> = {};\nexport const initialItemState: PaginatedMovieResult = {\n  page: 0,\n  results: [],\n  total_pages: 0,\n  total_results: 0,\n};\n\nconst discoverSlice = createSlice({\n  name: \"discover\",\n  initialState,\n  reducers: {\n    setNextPage: (state, action) => {\n      const { mediaType, itemKey } = action.payload;\n      state[mediaType][itemKey].page += 1;\n    },\n    initiateItem: (state, action) => {\n      const { mediaType, itemKey } = action.payload;\n      if (!state[mediaType]) {\n        state[mediaType] = {};\n      }\n      if (!state[mediaType][itemKey]) {\n        state[mediaType][itemKey] = initialItemState;\n      }\n    },\n  },\n  extraReducers(builder) {\n    builder.addMatcher(\n      isAnyOf(\n        extendedApi.endpoints.getVideosByMediaTypeAndCustomGenre.matchFulfilled,\n        extendedApi.endpoints.getVideosByMediaTypeAndGenreId.matchFulfilled\n      ),\n      (state, action) => {\n        const {\n          page,\n          results,\n          total_pages,\n          total_results,\n          mediaType,\n          itemKey,\n        } = action.payload;\n        state[mediaType][itemKey].page = page;\n        state[mediaType][itemKey].results.push(...results);\n        state[mediaType][itemKey].total_pages = total_pages;\n        state[mediaType][itemKey].total_results = total_results;\n      }\n    );\n  },\n});\n\nexport const { setNextPage, initiateItem } = discoverSlice.actions;\nexport default discoverSlice.reducer;\n\nconst extendedApi = tmdbApi.injectEndpoints({\n  endpoints: (build) => ({\n    getVideosByMediaTypeAndGenreId: build.query<\n      PaginatedMovieResult & {\n        mediaType: MEDIA_TYPE;\n        itemKey: number | string;\n      },\n      { mediaType: MEDIA_TYPE; genreId: number; page: number }\n    >({\n      query: ({ mediaType, genreId, page }) => ({\n        url: `/discover/${mediaType}`,\n        params: { api_key: TMDB_V3_API_KEY, with_genres: genreId, page },\n      }),\n      transformResponse: (\n        response: PaginatedMovieResult,\n        _,\n        { mediaType, genreId }\n      ) => ({\n        ...response,\n        mediaType,\n        itemKey: genreId,\n      }),\n    }),\n    getVideosByMediaTypeAndCustomGenre: build.query<\n      PaginatedMovieResult & {\n        mediaType: MEDIA_TYPE;\n        itemKey: number | string;\n      },\n      { mediaType: MEDIA_TYPE; apiString: string; page: number }\n    >({\n      query: ({ mediaType, apiString, page }) => ({\n        url: `/${mediaType}/${apiString}`,\n        params: { api_key: TMDB_V3_API_KEY, page },\n      }),\n      transformResponse: (\n        response: PaginatedMovieResult,\n        _,\n        { mediaType, apiString }\n      ) => {\n        return {\n          ...response,\n          mediaType,\n          itemKey: apiString,\n        };\n      },\n    }),\n    getAppendedVideos: build.query<\n      MovieDetail,\n      { mediaType: MEDIA_TYPE; id: number }\n    >({\n      query: ({ mediaType, id }) => ({\n        url: `/${mediaType}/${id}`,\n        params: { api_key: TMDB_V3_API_KEY, append_to_response: \"videos\" },\n      }),\n    }),\n    getSimilarVideos: build.query<\n      PaginatedMovieResult,\n      { mediaType: MEDIA_TYPE; id: number }\n    >({\n      query: ({ mediaType, id }) => ({\n        url: `/${mediaType}/${id}/similar`,\n        params: { api_key: TMDB_V3_API_KEY },\n      }),\n    }),\n  }),\n});\n\nexport const {\n  useGetVideosByMediaTypeAndGenreIdQuery,\n  useLazyGetVideosByMediaTypeAndGenreIdQuery,\n  useGetVideosByMediaTypeAndCustomGenreQuery,\n  useLazyGetVideosByMediaTypeAndCustomGenreQuery,\n  useGetAppendedVideosQuery,\n  useLazyGetAppendedVideosQuery,\n  useGetSimilarVideosQuery,\n  useLazyGetSimilarVideosQuery,\n} = extendedApi;\n"
  },
  {
    "path": "src/store/slices/genre.ts",
    "content": "import { TMDB_V3_API_KEY } from \"src/constant\";\nimport { Genre } from \"src/types/Genre\";\nimport { tmdbApi } from \"./apiSlice\";\n\nconst extendedApi = tmdbApi.injectEndpoints({\n  endpoints: (build) => ({\n    getGenres: build.query<Genre[], string>({\n      query: (mediaType) => ({\n        url: `/genre/${mediaType}/list`,\n        params: { api_key: TMDB_V3_API_KEY },\n      }),\n      transformResponse: (response: { genres: Genre[] }) => {\n        return response.genres;\n      },\n    }),\n  }),\n});\n\nexport const { useGetGenresQuery, endpoints: genreSliceEndpoints  } = extendedApi;\n"
  },
  {
    "path": "src/theme/palette.ts",
    "content": "import type { PaletteMode } from \"@mui/material\";\n\nconst PRIMARY = {\n  light: \"#B8B8B8\",\n  main: \"#141414\",\n  dark: \"#0E0A0A\",\n};\nconst GREY = {\n  100: \"#F9FAFB\",\n  200: \"#F4F6F8\",\n  300: \"#DFE3E8\",\n  400: \"#C4CDD5\",\n  500: \"#919EAB\",\n  600: \"#637381\",\n  700: \"#454F5B\",\n  800: \"#212B36\",\n  900: \"#161C24\",\n};\n\nconst COMMON = {\n  common: { black: \"#000\", white: \"#fff\" },\n  primary: { ...PRIMARY, contrastText: \"#fff\" },\n  grey: GREY,\n  action: {\n    active: GREY[500],\n    hoverOpacity: 0.08,\n    disabledOpacity: 0.48,\n  },\n};\n\nconst palette = {\n  ...COMMON,\n  text: { primary: \"#fff\", secondary: GREY[500], disabled: GREY[600] },\n  background: { default: PRIMARY.main, paper: PRIMARY.main },\n  mode: \"dark\" as PaletteMode,\n};\n\nexport default palette;\n"
  },
  {
    "path": "src/types/Common.ts",
    "content": "import { Movie } from \"src/types/Movie\";\n\nexport enum MEDIA_TYPE {\n  Movie = \"movie\",\n  Tv = \"tv\",\n}\n\nexport type Company = {\n  description: string;\n  headquarters: string;\n  homepage: string;\n  id: number;\n  logo_path: string;\n  name: string;\n  origin_country: string;\n  parent_company: null | object;\n};\n\nexport type Country = {\n  iso_3166_1: string;\n  english_name: string;\n};\n\nexport type Language = {\n  iso_639_1: string;\n  english_name: string;\n  name: string;\n};\n\nexport type PaginatedResult = {\n  page: number;\n  total_pages: number;\n  total_results: number;\n};\n\nexport type PaginatedMovieResult = PaginatedResult & { results: Movie[] };\n"
  },
  {
    "path": "src/types/Genre.ts",
    "content": "export type Genre = {\n  id: number;\n  name: string;\n};\n\nexport type CustomGenre = {\n  id?: number;\n  name: string;\n  apiString: string;\n};\n"
  },
  {
    "path": "src/types/Movie.ts",
    "content": "import { Company, Country, Language } from './Common';\nimport { Genre } from './Genre';\n\nexport type Appended_Video = {\n  id: string;\n  iso_639_1: string;\n  iso_3166_1: string;\n  key: string;\n  name: string;\n  official: boolean;\n  published_at: string;\n  site: string;\n  size: number;\n  type: string;\n};\n\nexport type MovieDetail = {\n  adult: boolean;\n  backdrop_path: string | null;\n  belongs_to_collection: null;\n  budget: number;\n  genres: Genre[];\n  homepage: string;\n  id: number;\n  imdb_id: string;\n  original_language: string;\n  original_title: string;\n  overview: string;\n  popularity: number;\n  poster_path: string | null;\n  production_companies: Company[];\n  production_countries: Country[];\n  release_date: string;\n  revenue: number;\n  runtime: number;\n  spoken_languages: Language[];\n  status: string;\n  tagline: string;\n  title: string;\n  video: boolean;\n  videos: { results: Appended_Video[] };\n  vote_average: number;\n  vote_count: number;\n};\n\nexport type Movie = {\n  poster_path: string | null;\n  adult: boolean;\n  overview: string;\n  release_date: string;\n  genre_ids: number[];\n  id: number;\n  original_title: string;\n  original_language: string;\n  title: string;\n  backdrop_path: string | null;\n  popularity: number;\n  vote_count: number;\n  video: boolean;\n  vote_average: number;\n};\n"
  },
  {
    "path": "src/utils/common.ts",
    "content": "export const getRandomNumber = (maxNumber: number) =>\n  Math.floor(Math.random() * maxNumber);\n\nexport const formatMinuteToReadable = (minutes: number) => {\n  const h = Math.floor(minutes / 60);\n  const m = minutes - h * 60;\n\n  if (h > 0) {\n    return `${h}h ${m}m`;\n  } else {\n    return `${m}m`;\n  }\n};\n\nexport const formatBytes = (bytes: number, decimals: number = 2) => {\n  if (!+bytes) return \"0 Bytes\";\n\n  const k = 1024;\n  const dm = decimals < 0 ? 0 : decimals;\n  const sizes = [\n    \"Bytes\",\n    \"KiB\",\n    \"MiB\",\n    \"GiB\",\n    \"TiB\",\n    \"PiB\",\n    \"EiB\",\n    \"ZiB\",\n    \"YiB\",\n  ];\n\n  const i = Math.floor(Math.log(bytes) / Math.log(k));\n\n  return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;\n};\n\nexport const formatTime = (current: number) => {\n  const h = Math.floor(current / 3600);\n  const m = Math.floor((current - h * 3600) / 60);\n  const s = Math.floor(current % 60);\n\n  const sString = s < 10 ? \"0\" + s.toString() : s.toString();\n  const mString = m < 10 ? \"0\" + m.toString() : m.toString();\n\n  if (h > 0) {\n    const hString = h < 10 ? \"0\" + h.toString() : h.toString();\n    return `${hString}:${mString}:${sString}`;\n  } else {\n    return `${mString}:${sString}`;\n  }\n};\n"
  },
  {
    "path": "src/utils/index.ts",
    "content": "export { formatMinuteToReadable, getRandomNumber } from \"./common\";\n\nfunction buildThresholdList() {\n  let thresholds = [];\n  const numSteps = 20;\n\n  for (let i = 1.0; i <= numSteps; i++) {\n    let ratio = i / numSteps;\n    thresholds.push(ratio);\n  }\n\n  thresholds.push(0);\n  return thresholds;\n}\n\nexport { buildThresholdList };\n"
  },
  {
    "path": "src/videojs-youtube.d.ts",
    "content": "declare module \"videojs-youtube\" {}\r\n"
  },
  {
    "path": "src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"allowJs\": false,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": false,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n    \"baseUrl\": \".\"\n  },\n  \"include\": [\"src\"],\n  \"references\": [{ \"path\": \"./tsconfig.node.json\" }]\n}\n"
  },
  {
    "path": "tsconfig.node.json",
    "content": "{\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Node\",\n    \"allowSyntheticDefaultImports\": true\n  },\n  \"include\": [\"vite.config.ts\"]\n}\n"
  },
  {
    "path": "vercel.json",
    "content": "{\r\n  \"rewrites\": [{ \"source\": \"/(.*)\", \"destination\": \"/\" }]\r\n}\r\n"
  },
  {
    "path": "vite.config.ts",
    "content": "import { defineConfig } from 'vite'\nimport react from '@vitejs/plugin-react'\nimport tsconfigPaths from 'vite-tsconfig-paths'\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  plugins: [react(), tsconfigPaths()]\n})\n"
  }
]