├── .gitignore
├── README.md
├── index.html
├── package-lock.json
├── package.json
├── public
├── movix-logo.png
└── vite.svg
├── src
├── App.jsx
├── assets
│ └── images
│ │ ├── avatar.png
│ │ ├── movix-logo.svg
│ │ ├── no-poster.png
│ │ └── no-results.png
├── components
│ ├── carousel
│ │ ├── Carousel.jsx
│ │ └── style.scss
│ ├── circleRating
│ │ ├── CircleRating.jsx
│ │ └── style.scss
│ ├── contentWrapper
│ │ ├── ContentWrapper.jsx
│ │ └── style.scss
│ ├── footer
│ │ ├── Footer.jsx
│ │ └── footer.scss
│ ├── genres
│ │ ├── Genres.jsx
│ │ └── style.scss
│ ├── header
│ │ ├── Header.jsx
│ │ └── header.scss
│ ├── lazyImg
│ │ └── Img.jsx
│ ├── movieCard
│ │ ├── MovieCard.jsx
│ │ └── style.scss
│ ├── spinner
│ │ ├── Spinner.jsx
│ │ └── style.scss
│ ├── switchTabs
│ │ ├── SwitchTabs.jsx
│ │ └── style.scss
│ └── videoPopup
│ │ ├── VideoPopup.jsx
│ │ └── style.scss
├── hooks
│ └── useFetch.jsx
├── index.scss
├── main.jsx
├── mixin.scss
├── pages
│ ├── 404
│ │ ├── 404.jsx
│ │ └── style.scss
│ ├── details
│ │ ├── Details.jsx
│ │ ├── PlayBtn.jsx
│ │ ├── carousels
│ │ │ ├── Recommendation.jsx
│ │ │ └── Similar.jsx
│ │ ├── cast
│ │ │ ├── Cast.jsx
│ │ │ └── style.scss
│ │ ├── detailsBanner
│ │ │ ├── DetailsBanner.jsx
│ │ │ └── style.scss
│ │ ├── style.scss
│ │ └── videoSection
│ │ │ ├── VideoSection.jsx
│ │ │ └── style.scss
│ ├── explore
│ │ ├── Explore.jsx
│ │ └── style.scss
│ ├── home
│ │ ├── Home.jsx
│ │ ├── heroBanner
│ │ │ ├── HeroBanner.jsx
│ │ │ └── style.scss
│ │ ├── popular
│ │ │ └── Popular.jsx
│ │ ├── style.scss
│ │ ├── topRated
│ │ │ └── TopRated.jsx
│ │ └── trending
│ │ │ └── Trending.jsx
│ └── searchResult
│ │ ├── SearchResult.jsx
│ │ └── style.scss
├── store
│ ├── homeSlice.js
│ └── store.js
└── utils
│ └── api.js
└── vite.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 | .env
15 |
16 | # Editor directories and files
17 | .vscode/*
18 | !.vscode/extensions.json
19 | .idea
20 | .DS_Store
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
Movix_App
2 |
3 | This is a movie listing app made using TMDB api.
4 |
5 | 🚀 Demo
6 |
7 | [https://movixzilla.netlify.app/](https://movixzilla.netlify.app/)
8 |
9 | Project Screenshots:
10 |
11 |
12 |
13 |
14 |
15 |
16 | 🛠️ Installation Steps:
17 |
18 | 1. Clone the repo
19 |
20 | ```
21 | https://github.com/mukeshpandey9/Movix_App.git
22 | ```
23 |
24 | 2. Install all the node modules
25 |
26 | ```
27 | npm install
28 | ```
29 |
30 | 3. Run the server
31 |
32 | ```
33 | npm run dev
34 | ```
35 |
36 | 4. That's it web app live on
37 |
38 | ```
39 | localhost:5137
40 | ```
41 |
42 | Technologies Used:-
43 | 1. React
44 | 2. Redux
45 | 3. TMDB Api
46 | 4. Sass
47 | 5. Axios
48 | 6. Lazy loading
49 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Movix
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vite-project",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "@reduxjs/toolkit": "^1.9.1",
14 | "axios": "^1.2.2",
15 | "dayjs": "^1.11.7",
16 | "react": "^18.2.0",
17 | "react-circular-progressbar": "^2.1.0",
18 | "react-dom": "^18.2.0",
19 | "react-icons": "^4.7.1",
20 | "react-infinite-scroll-component": "^6.1.0",
21 | "react-lazy-load-image-component": "^1.5.6",
22 | "react-player": "^2.11.0",
23 | "react-redux": "^8.0.5",
24 | "react-router-dom": "^6.6.2",
25 | "react-select": "^5.7.0",
26 | "sass": "^1.57.1"
27 | },
28 | "devDependencies": {
29 | "@types/react": "^18.2.15",
30 | "@types/react-dom": "^18.2.7",
31 | "@vitejs/plugin-react": "^4.0.3",
32 | "eslint": "^8.45.0",
33 | "eslint-plugin-react": "^7.32.2",
34 | "eslint-plugin-react-hooks": "^4.6.0",
35 | "eslint-plugin-react-refresh": "^0.4.3",
36 | "vite": "^4.4.5"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/public/movix-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mukeshpandey9/Movix_App/188cb0f44523d5d6a7ce52382e92f29107101ce6/public/movix-logo.png
--------------------------------------------------------------------------------
/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/App.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import { fetchDataFromApi } from "./utils/api";
3 | import { useSelector, useDispatch } from "react-redux";
4 | import { getApiConfiguration, getGenres } from "./store/homeSlice";
5 | import { BrowserRouter, Routes, Route } from "react-router-dom";
6 | import Home from "./pages/home/Home";
7 | import PageNotFound from "./pages/404/404";
8 | import Header from "./components/header/Header";
9 | import Footer from "./components/footer/Footer";
10 | import Details from "./pages/details/Details";
11 | import SearchResult from "./pages/searchResult/SearchResult";
12 | import Explore from "./pages/explore/Explore";
13 | function App() {
14 | const dispatch = useDispatch();
15 | const { url } = useSelector((state) => state.home);
16 | useEffect(() => {
17 | fetchApiConfig();
18 | genresCall();
19 | }, []);
20 |
21 | const fetchApiConfig = () => {
22 | fetchDataFromApi("/configuration").then((res) => {
23 | console.log(res);
24 | const url = {
25 | backdrop: res.images.secure_base_url + "original",
26 | poster: res.images.secure_base_url + "original",
27 | profilr: res.images.secure_base_url + "original",
28 | };
29 | dispatch(getApiConfiguration(url));
30 | });
31 | };
32 |
33 | const genresCall = async () => {
34 | let promises = [];
35 | let endPoints = ["tv", "movie"];
36 | let allGenres = {};
37 |
38 | endPoints.forEach((url) => {
39 | promises.push(fetchDataFromApi(`/genre/${url}/list`));
40 | });
41 |
42 | const data = await Promise.all(promises);
43 |
44 | data.map(({ genres }) => {
45 | return genres.map((item) => (allGenres[item.id] = item));
46 | });
47 |
48 | dispatch(getGenres(allGenres));
49 | };
50 | return (
51 | <>
52 |
53 |
54 |
55 | } />
56 | } />
57 | } />
58 | } />
59 | } />
60 |
61 |
62 |
63 |
64 | >
65 | );
66 | }
67 |
68 | export default App;
69 |
--------------------------------------------------------------------------------
/src/assets/images/avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mukeshpandey9/Movix_App/188cb0f44523d5d6a7ce52382e92f29107101ce6/src/assets/images/avatar.png
--------------------------------------------------------------------------------
/src/assets/images/movix-logo.svg:
--------------------------------------------------------------------------------
1 |
17 |
--------------------------------------------------------------------------------
/src/assets/images/no-poster.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mukeshpandey9/Movix_App/188cb0f44523d5d6a7ce52382e92f29107101ce6/src/assets/images/no-poster.png
--------------------------------------------------------------------------------
/src/assets/images/no-results.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mukeshpandey9/Movix_App/188cb0f44523d5d6a7ce52382e92f29107101ce6/src/assets/images/no-results.png
--------------------------------------------------------------------------------
/src/components/carousel/Carousel.jsx:
--------------------------------------------------------------------------------
1 | import React, { useRef } from "react";
2 | import {
3 | BsFillArrowLeftCircleFill,
4 | BsFillArrowRightCircleFill,
5 | } from "react-icons/bs";
6 | import { useNavigate } from "react-router-dom";
7 | import { useSelector } from "react-redux";
8 | import dayjs from "dayjs";
9 |
10 | import ContentWrapper from "../contentWrapper/ContentWrapper";
11 | import Img from "../lazyImg/Img";
12 | import PosterFallback from "../../assets/images/no-poster.png";
13 |
14 | import "./style.scss";
15 | import CircleRating from "../circleRating/CircleRating";
16 | import Genres from "../genres/Genres";
17 |
18 | const Carousel = ({ data, loading, endpoint, title }) => {
19 | const carouselContainer = useRef();
20 | const { url } = useSelector((state) => state.home);
21 | const navigate = useNavigate();
22 |
23 | // Scroll Effect
24 | const navigation = (dir) => {
25 | const container = carouselContainer.current;
26 |
27 | const scrollAmount =
28 | dir === "left"
29 | ? container.scrollLeft - (container.offsetWidth + 20)
30 | : container.scrollLeft + (container.offsetWidth + 20);
31 |
32 | container.scrollTo({
33 | left: scrollAmount,
34 | behavior: "smooth",
35 | });
36 | };
37 |
38 | const skItem = () => {
39 | return (
40 |
47 | );
48 | };
49 | return (
50 |
51 |
52 | {title && {title}
}
53 | navigation("left")}
56 | />
57 | navigation("right")}
60 | />
61 |
62 | {!loading ? (
63 |
64 | {data?.map((item) => {
65 | const posterUrl = item.poster_path
66 | ? url.poster + item.poster_path
67 | : PosterFallback;
68 | return (
69 |
73 | navigate(`/${item.media_type || endpoint}/${item.id}`)
74 | }
75 | >
76 |
77 |

78 |
79 |
80 |
81 |
82 | {item.title || item.name}
83 |
84 | {dayjs(item.release_date || item.first_air_date).format(
85 | "MMM D, YYYY"
86 | )}
87 |
88 |
89 |
90 | );
91 | })}
92 |
93 | ) : (
94 |
95 | {skItem()}
96 | {skItem()}
97 | {skItem()}
98 | {skItem()}
99 | {skItem()}
100 | {skItem()}
101 | {skItem()}
102 | {skItem()}
103 | {skItem()}
104 | {skItem()}
105 | {skItem()}
106 |
107 | )}
108 |
109 |
110 | );
111 | };
112 |
113 | export default Carousel;
114 |
--------------------------------------------------------------------------------
/src/components/carousel/style.scss:
--------------------------------------------------------------------------------
1 | @import "../../mixin.scss";
2 | .carousel {
3 | margin-bottom: 50px;
4 | .contentWrapper {
5 | position: relative;
6 | }
7 | .carouselTitle {
8 | font-size: 24px;
9 | color: white;
10 | margin-bottom: 20px;
11 | font-weight: normal;
12 | }
13 | .arrow {
14 | font-size: 30px;
15 | color: black;
16 | position: absolute;
17 | top: 44%;
18 | transform: translateY(-50%);
19 | cursor: pointer;
20 | opacity: 0.5;
21 | z-index: 1;
22 | display: none;
23 | @include md {
24 | display: block;
25 | }
26 | &:hover {
27 | opacity: 0.8;
28 | }
29 | }
30 | .carouselLeftNav {
31 | left: 30px;
32 | }
33 | .carouselRightNav {
34 | right: 30px;
35 | }
36 | .loadingSkeleton {
37 | display: flex;
38 | gap: 10px;
39 | overflow-y: hidden;
40 | margin-right: -20px;
41 | margin-left: -20px;
42 | padding: 0 20px;
43 | @include md {
44 | gap: 20px;
45 | overflow: hidden;
46 | margin: 0;
47 | padding: 0;
48 | }
49 | .skeletonItem {
50 | width: 125px;
51 | @include md {
52 | width: calc(25% - 15px);
53 | }
54 | @include lg {
55 | width: calc(20% - 16px);
56 | }
57 | flex-shrink: 0;
58 | .posterBlock {
59 | border-radius: 12px;
60 | width: 100%;
61 | aspect-ratio: 1 / 1.5;
62 | margin-bottom: 30px;
63 | }
64 | .textBlock {
65 | display: flex;
66 | flex-direction: column;
67 | .title {
68 | width: 100%;
69 | height: 20px;
70 | margin-bottom: 10px;
71 | }
72 | .date {
73 | width: 75%;
74 | height: 20px;
75 | }
76 | }
77 | }
78 | }
79 | .carouselItems {
80 | display: flex;
81 | gap: 10px;
82 | overflow-y: hidden;
83 | margin-right: -20px;
84 | margin-left: -20px;
85 | padding: 0 20px;
86 | @include md {
87 | gap: 20px;
88 | overflow: hidden;
89 | margin: 0;
90 | padding: 0;
91 | }
92 | .carouselItem {
93 | width: 125px;
94 | cursor: pointer;
95 | @include md {
96 | width: calc(25% - 15px);
97 | }
98 | @include lg {
99 | width: calc(20% - 16px);
100 | }
101 | flex-shrink: 0;
102 | .posterBlock {
103 | position: relative;
104 | width: 100%;
105 | aspect-ratio: 1 / 1.5;
106 | background-size: cover;
107 | background-position: center;
108 | margin-bottom: 30px;
109 | display: flex;
110 | align-items: flex-end;
111 | justify-content: space-between;
112 | padding: 10px;
113 | .lazy-load-image-background {
114 | position: absolute;
115 | top: 0;
116 | left: 0;
117 | width: 100%;
118 | height: 100%;
119 | border-radius: 12px;
120 | overflow: hidden;
121 | img {
122 | width: 100%;
123 | height: 100%;
124 | object-fit: cover;
125 | object-position: center;
126 | }
127 | }
128 | .circleRating {
129 | width: 40px;
130 | height: 40px;
131 | position: relative;
132 | top: 30px;
133 | background-color: white;
134 | flex-shrink: 0;
135 | @include md {
136 | width: 50px;
137 | height: 50px;
138 | }
139 | }
140 | .genres {
141 | display: none;
142 | position: relative;
143 | @include md {
144 | display: flex;
145 | flex-flow: wrap;
146 | justify-content: flex-end;
147 | }
148 | }
149 | }
150 | .textBlock {
151 | color: white;
152 | display: flex;
153 | flex-direction: column;
154 | .title {
155 | font-size: 16px;
156 | margin-bottom: 10px;
157 | line-height: 24px;
158 | @include ellipsis(1);
159 | @include md {
160 | font-size: 20px;
161 | }
162 | }
163 | .date {
164 | font-size: 14px;
165 | opacity: 0.5;
166 | }
167 | }
168 | }
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/src/components/circleRating/CircleRating.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { CircularProgressbar, buildStyles } from "react-circular-progressbar";
3 | import "react-circular-progressbar/dist/styles.css";
4 |
5 | import "./style.scss";
6 |
7 | const CircleRating = ({ rating }) => {
8 | return (
9 |
10 |
18 |
19 | );
20 | };
21 |
22 | export default CircleRating;
23 |
--------------------------------------------------------------------------------
/src/components/circleRating/style.scss:
--------------------------------------------------------------------------------
1 | .circleRating {
2 | background-color: var(--black);
3 | border-radius: 50%;
4 | padding: 2px;
5 | .CircularProgressbar-text {
6 | font-size: 34px;
7 | font-weight: 700;
8 | fill: var(--black);
9 | }
10 | .CircularProgressbar-trail {
11 | stroke: transparent;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/contentWrapper/ContentWrapper.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import "./style.scss";
4 |
5 | const ContentWrapper = ({ children }) => {
6 | return {children}
;
7 | };
8 |
9 | export default ContentWrapper;
10 |
--------------------------------------------------------------------------------
/src/components/contentWrapper/style.scss:
--------------------------------------------------------------------------------
1 | @import "../../mixin.scss";
2 | .contentWrapper {
3 | width: 100%;
4 | max-width: 1200px;
5 | margin: 0 auto;
6 | padding: 0 5vw;
7 | @include md {
8 | padding: 0 10vw;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/components/footer/Footer.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | FaFacebookF,
4 | FaInstagram,
5 | FaTwitter,
6 | FaLinkedin,
7 | } from "react-icons/fa";
8 |
9 | import ContentWrapper from "../contentWrapper/ContentWrapper";
10 |
11 | import "./footer.scss";
12 |
13 | const Footer = () => {
14 | return (
15 |
48 | );
49 | };
50 |
51 | export default Footer;
52 |
--------------------------------------------------------------------------------
/src/components/footer/footer.scss:
--------------------------------------------------------------------------------
1 | @import "../../mixin.scss";
2 | .footer {
3 | background-color: var(--black3);
4 | padding: 50px 0;
5 | color: white;
6 | position: relative;
7 | .contentWrapper {
8 | display: flex;
9 | align-items: center;
10 | flex-direction: column;
11 | }
12 | .menuItems {
13 | list-style-type: none;
14 | display: flex;
15 | align-items: center;
16 | justify-content: center;
17 | gap: 15px;
18 | margin-bottom: 20px;
19 | @include md {
20 | margin-bottom: 30px;
21 | gap: 30px;
22 | }
23 | .menuItem {
24 | transition: all ease 0.3s;
25 | cursor: pointer;
26 | font-size: 12px;
27 | @include md {
28 | font-size: 16px;
29 | }
30 | &:hover {
31 | color: var(--pink);
32 | }
33 | }
34 | }
35 | .infoText {
36 | font-size: 12px;
37 | line-height: 20px;
38 | opacity: 0.5;
39 | text-align: center;
40 | max-width: 800px;
41 | margin-bottom: 20px;
42 | @include md {
43 | font-size: 14px;
44 | margin-bottom: 30px;
45 | }
46 | }
47 | .socialIcons {
48 | display: flex;
49 | align-items: center;
50 | justify-content: center;
51 | gap: 10px;
52 | .icon {
53 | width: 50px;
54 | height: 50px;
55 | border-radius: 50%;
56 | background-color: var(--black);
57 | display: flex;
58 | align-items: center;
59 | justify-content: center;
60 | cursor: pointer;
61 | transition: all ease 0.3s;
62 | &:hover {
63 | box-shadow: 0 0 0.625em var(--pink);
64 | color: var(--pink);
65 | }
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/components/genres/Genres.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useSelector } from "react-redux";
3 | import "./style.scss";
4 | const Genres = ({ data }) => {
5 | const { genres } = useSelector((state) => state.home);
6 | return (
7 |
8 | {data?.map((g) => {
9 | if (!genres[g]?.name) return;
10 | return (
11 |
12 | {genres[g]?.name}
13 |
14 | );
15 | })}
16 |
17 | );
18 | };
19 |
20 | export default Genres;
21 |
--------------------------------------------------------------------------------
/src/components/genres/style.scss:
--------------------------------------------------------------------------------
1 | .genres {
2 | display: flex;
3 | gap: 5px;
4 | .genre {
5 | background-color: var(--pink);
6 | padding: 3px 5px;
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/components/header/Header.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { HiOutlineSearch } from "react-icons/hi";
3 | import { SlMenu } from "react-icons/sl";
4 | import { VscChromeClose } from "react-icons/vsc";
5 | import { useNavigate, useLocation, Link } from "react-router-dom";
6 |
7 | import "./header.scss";
8 |
9 | import logo from "../../assets/images/movix-logo.svg";
10 | import ContentWrapper from "../contentWrapper/ContentWrapper";
11 |
12 | const Header = () => {
13 | const [show, setShow] = useState("top");
14 | const [lastScrollY, setLastScrollY] = useState(0);
15 | const [mobileMenu, setMobileMenu] = useState(false);
16 | const [query, setQuery] = useState("");
17 | const [showSearch, setShowSearch] = useState("");
18 | const navigate = useNavigate();
19 | const location = useLocation();
20 |
21 | useEffect(() => {
22 | window.scrollTo(0, 0);
23 | }, [location]);
24 |
25 | const controlNavbar = () => {
26 | if (window.scrollY > 300) {
27 | if (window.scrollY > lastScrollY && !mobileMenu) {
28 | setShow("hide");
29 | } else {
30 | setShow("show");
31 | }
32 | } else {
33 | setShow("top");
34 | }
35 | setLastScrollY(window.scrollY);
36 | };
37 | useEffect(() => {
38 | window.addEventListener("scroll", controlNavbar);
39 |
40 | return () => {
41 | window.addEventListener("scroll", controlNavbar);
42 | };
43 | }, [lastScrollY]);
44 |
45 | const searchQueryHandler = (event) => {
46 | if (event.key === "Enter" && query.length > 0) {
47 | navigate(`/search/${query}`);
48 | setTimeout(() => {
49 | setShowSearch(false);
50 | }, 1000);
51 | }
52 | };
53 |
54 | const openSearch = () => {
55 | setMobileMenu(false);
56 | setShowSearch(true);
57 | };
58 | const openMobileView = () => {
59 | setMobileMenu(true);
60 | setShowSearch(false);
61 | };
62 |
63 | const navigationHandler = (type) => {
64 | if (type === "movie") {
65 | navigate("/explore/movie");
66 | } else {
67 | navigate("/explore/tv");
68 | }
69 |
70 | setMobileMenu(false);
71 | };
72 |
73 | return (
74 |
118 | );
119 | };
120 |
121 | export default Header;
122 |
--------------------------------------------------------------------------------
/src/components/header/header.scss:
--------------------------------------------------------------------------------
1 | @import "../../mixin.scss";
2 | .header {
3 | position: fixed;
4 | transform: translateY(0);
5 | width: 100%;
6 | height: 60px;
7 | z-index: 1;
8 | display: flex;
9 | align-items: center;
10 | transition: all ease 0.5s;
11 | z-index: 2;
12 | &.top {
13 | background: rgba(0, 0, 0, 0.2);
14 | backdrop-filter: blur(3.5px);
15 | -webkit-backdrop-filter: blur(3.5px);
16 | }
17 | &.show {
18 | background-color: var(--black3);
19 | }
20 | &.hide {
21 | transform: translateY(-60px);
22 | }
23 |
24 | .contentWrapper {
25 | display: flex;
26 | align-items: center;
27 | justify-content: space-between;
28 | }
29 | .logo {
30 | cursor: pointer;
31 | img {
32 | height: 50px;
33 | }
34 | }
35 | .menuItems {
36 | list-style-type: none;
37 | display: none;
38 | align-items: center;
39 | @include md {
40 | display: flex;
41 | }
42 | .menuItem {
43 | height: 60px;
44 | display: flex;
45 | align-items: center;
46 | margin: 0 15px;
47 | color: white;
48 | font-weight: 500;
49 | position: relative;
50 | &.searchIcon {
51 | margin-right: 0;
52 | }
53 | svg {
54 | font-size: 18px;
55 | }
56 | cursor: pointer;
57 | &:hover {
58 | color: var(--pink);
59 | }
60 | }
61 | }
62 |
63 | .mobileMenuItems {
64 | display: flex;
65 | align-items: center;
66 | gap: 20px;
67 | @include md {
68 | display: none;
69 | }
70 | svg {
71 | font-size: 18px;
72 | color: white;
73 | }
74 | }
75 |
76 | &.mobileView {
77 | background: var(--black3);
78 | .menuItems {
79 | display: flex;
80 | position: absolute;
81 | top: 60px;
82 | left: 0;
83 | background: var(--black3);
84 | flex-direction: column;
85 | width: 100%;
86 | padding: 20px 0;
87 | border-top: 1px solid rgba(255, 255, 255, 0.1);
88 | animation: mobileMenu 0.3s ease forwards;
89 | .menuItem {
90 | font-size: 20px;
91 | width: 100%;
92 | height: auto;
93 | padding: 15px 20px;
94 | margin: 0;
95 | display: flex;
96 | flex-direction: column;
97 | align-items: flex-start;
98 | &:last-child {
99 | display: none;
100 | }
101 | }
102 | }
103 | }
104 |
105 | .searchBar {
106 | width: 100%;
107 | height: 60px;
108 | background-color: white;
109 | position: absolute;
110 | top: 60px;
111 | animation: mobileMenu 0.3s ease forwards;
112 | .searchInput {
113 | display: flex;
114 | align-items: center;
115 | height: 40px;
116 | margin-top: 10px;
117 | width: 100%;
118 | svg {
119 | font-size: 20px;
120 | flex-shrink: 0;
121 | margin-left: 10px;
122 | cursor: pointer;
123 | }
124 | input {
125 | width: 100%;
126 | height: 50px;
127 | background-color: white;
128 | outline: 0;
129 | border: 0;
130 | border-radius: 30px 0 0 30px;
131 | padding: 0 15px;
132 | font-size: 14px;
133 | @include md {
134 | height: 60px;
135 | font-size: 20px;
136 | padding: 0 30px;
137 | }
138 | }
139 | }
140 | }
141 | }
142 |
143 | @keyframes mobileMenu {
144 | 0% {
145 | transform: translateY(-130%);
146 | }
147 | 100% {
148 | transform: translateY(0);
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/src/components/lazyImg/Img.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { LazyLoadImage } from "react-lazy-load-image-component";
3 | import "react-lazy-load-image-component/src/effects/blur.css";
4 |
5 | const Img = ({ src, className }) => {
6 | return (
7 |
8 | );
9 | };
10 |
11 | export default Img;
12 |
--------------------------------------------------------------------------------
/src/components/movieCard/MovieCard.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import dayjs from "dayjs";
3 | import { useNavigate } from "react-router-dom";
4 | import { useSelector } from "react-redux";
5 |
6 | import "./style.scss";
7 | import Img from "../lazyImg/Img";
8 | import CircleRating from "../circleRating/CircleRating";
9 | import Genres from "../genres/Genres";
10 | import PosterFallback from "../../assets/images/no-poster.png";
11 |
12 | const MovieCard = ({ data, fromSearch, mediaType }) => {
13 | const { url } = useSelector((state) => state.home);
14 | const navigate = useNavigate();
15 | const posterUrl = data.poster_path
16 | ? url.poster + data.poster_path
17 | : PosterFallback;
18 | return (
19 | navigate(`/${data.media_type || mediaType}/${data.id}`)}
22 | >
23 |
24 |

25 | {!fromSearch && (
26 |
27 |
28 |
29 |
30 | )}
31 |
32 |
33 | {data.title || data.name}
34 |
35 | {dayjs(data.release_date).format("MMM D, YYYY")}
36 |
37 |
38 |
39 | );
40 | };
41 |
42 | export default MovieCard;
43 |
--------------------------------------------------------------------------------
/src/components/movieCard/style.scss:
--------------------------------------------------------------------------------
1 | @import "../../mixin.scss";
2 | .movieCard {
3 | width: calc(50% - 5px);
4 | margin-bottom: 25px;
5 | cursor: pointer;
6 | flex-shrink: 0;
7 | @include md {
8 | width: calc(25% - 15px);
9 | }
10 | @include lg {
11 | width: calc(20% - 16px);
12 | }
13 | .posterBlock {
14 | position: relative;
15 | width: 100%;
16 | aspect-ratio: 1 / 1.5;
17 | background-size: cover;
18 | background-position: center;
19 | margin-bottom: 30px;
20 | display: flex;
21 | align-items: flex-end;
22 | justify-content: space-between;
23 | padding: 10px;
24 | transition: all ease 0.5s;
25 | .lazy-load-image-background {
26 | position: absolute;
27 | top: 0;
28 | left: 0;
29 | width: 100%;
30 | height: 100%;
31 | border-radius: 12px;
32 | overflow: hidden;
33 | img {
34 | width: 100%;
35 | height: 100%;
36 | object-fit: cover;
37 | object-position: center;
38 | }
39 | }
40 | .circleRating {
41 | width: 40px;
42 | height: 40px;
43 | position: relative;
44 | top: 30px;
45 | background-color: white;
46 | flex-shrink: 0;
47 | @include md {
48 | width: 50px;
49 | height: 50px;
50 | }
51 | }
52 | .genres {
53 | display: none;
54 | position: relative;
55 | @include md {
56 | display: flex;
57 | flex-flow: wrap;
58 | justify-content: flex-end;
59 | }
60 | }
61 | }
62 | .textBlock {
63 | color: white;
64 | display: flex;
65 | flex-direction: column;
66 | .title {
67 | font-size: 16px;
68 | margin-bottom: 10px;
69 | line-height: 24px;
70 | @include ellipsis(1);
71 | @include md {
72 | font-size: 20px;
73 | }
74 | }
75 | .date {
76 | font-size: 14px;
77 | opacity: 0.5;
78 | }
79 | }
80 | &:hover {
81 | .posterBlock {
82 | opacity: 0.5;
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/components/spinner/Spinner.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import "./style.scss";
4 |
5 | const Spinner = ({ initial }) => {
6 | return (
7 |
8 |
18 |
19 | );
20 | };
21 |
22 | export default Spinner;
23 |
--------------------------------------------------------------------------------
/src/components/spinner/style.scss:
--------------------------------------------------------------------------------
1 | .loadingSpinner {
2 | width: 100%;
3 | height: 150px;
4 | position: relative;
5 | display: flex;
6 | align-items: center;
7 | justify-content: center;
8 | .spinner {
9 | animation: rotate 2s linear infinite;
10 | z-index: 2;
11 | width: 50px;
12 | height: 50px;
13 | & .path {
14 | stroke: hsl(210, 70%, 75%);
15 | stroke-linecap: round;
16 | animation: dash 1.5s ease-in-out infinite;
17 | }
18 | }
19 |
20 | &.initial {
21 | height: 700px;
22 | }
23 |
24 | @keyframes rotate {
25 | 100% {
26 | transform: rotate(360deg);
27 | }
28 | }
29 |
30 | @keyframes dash {
31 | 0% {
32 | stroke-dasharray: 1, 150;
33 | stroke-dashoffset: 0;
34 | }
35 | 50% {
36 | stroke-dasharray: 90, 150;
37 | stroke-dashoffset: -35;
38 | }
39 | 100% {
40 | stroke-dasharray: 90, 150;
41 | stroke-dashoffset: -124;
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/components/switchTabs/SwitchTabs.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import "./style.scss";
3 | const SwitchTabs = ({ data, onTabChange }) => {
4 | const [selectedTab, setSelectedTab] = useState(0);
5 | const [left, setLeft] = useState(0);
6 | const activeTab = (tab, index) => {
7 | setLeft(index * 100);
8 | setTimeout(() => {
9 | setSelectedTab(index);
10 | }, 300);
11 | onTabChange(tab, index);
12 | };
13 | return (
14 |
15 |
16 | {data.map((tab, index) => {
17 | return (
18 | activeTab(tab, index)}
22 | >
23 | {tab}
24 |
25 | );
26 | })}
27 |
28 |
29 |
30 | );
31 | };
32 |
33 | export default SwitchTabs;
34 |
--------------------------------------------------------------------------------
/src/components/switchTabs/style.scss:
--------------------------------------------------------------------------------
1 | .switchingTabs {
2 | height: 34px;
3 | position: relative;
4 | background-color: white;
5 | border-radius: 20px;
6 | width: 210px;
7 | padding: 2px;
8 | .tabItems {
9 | display: flex;
10 | align-items: center;
11 | height: 30px;
12 | position: relative;
13 | .tabItem {
14 | height: 100%;
15 | display: flex;
16 | align-items: center;
17 | justify-content: center;
18 | width: 100px;
19 | color: var(--black);
20 | font-size: 14px;
21 | position: relative;
22 | z-index: 11;
23 | cursor: pointer;
24 | transition: color ease 0.3s;
25 | &.active {
26 | color: white;
27 | }
28 | }
29 | .movingBg {
30 | height: 30px;
31 | width: 100px;
32 | border-radius: 15px;
33 | background-image: var(--gradient);
34 | position: absolute;
35 | right: 0;
36 | transition: left cubic-bezier(0.88, -0.35, 0.565, 1.35) 0.4s;
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/components/videoPopup/VideoPopup.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactPlayer from "react-player/youtube";
3 |
4 | import "./style.scss";
5 |
6 | const VideoPopup = ({ show, setShow, videoId, setVideoId }) => {
7 | const hidePopup = () => {
8 | setShow(false);
9 | setVideoId(null);
10 | };
11 | return (
12 |
13 |
14 |
15 |
16 | Close
17 |
18 |
25 |
26 |
27 | );
28 | };
29 |
30 | export default VideoPopup;
31 |
--------------------------------------------------------------------------------
/src/components/videoPopup/style.scss:
--------------------------------------------------------------------------------
1 | .videoPopup {
2 | display: flex;
3 | justify-content: center;
4 | align-items: center;
5 | width: 100%;
6 | height: 100%;
7 | position: fixed;
8 | top: 0;
9 | left: 0;
10 | opacity: 0;
11 | visibility: hidden;
12 | z-index: 9;
13 | .opacityLayer {
14 | position: absolute;
15 | top: 0;
16 | left: 0;
17 | width: 100%;
18 | height: 100%;
19 | background: rgba(0, 0, 0, 0.25);
20 | backdrop-filter: blur(3.5px);
21 | -webkit-backdrop-filter: blur(3.5px);
22 | opacity: 0;
23 | transition: opacity 400ms;
24 | }
25 | .videoPlayer {
26 | position: relative;
27 | width: 800px;
28 | aspect-ratio: 16 / 9;
29 | background-color: white;
30 | transform: scale(0.2);
31 | transition: transform 250ms;
32 | .closeBtn {
33 | position: absolute;
34 | top: -20px;
35 | right: 0;
36 | color: white;
37 | cursor: pointer;
38 | }
39 | }
40 | &.visible {
41 | opacity: 1;
42 | visibility: visible;
43 | .opacityLayer {
44 | opacity: 1;
45 | }
46 | .videoPlayer {
47 | transform: scale(1);
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/hooks/useFetch.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { fetchDataFromApi } from "../utils/api";
3 | const useFetch = (url) => {
4 | const [data, setData] = useState(null);
5 | const [loading, setLoading] = useState(null);
6 | const [error, setError] = useState(null);
7 |
8 | useEffect(() => {
9 | setLoading("loading...");
10 | setData(null);
11 | setError(null);
12 |
13 | fetchDataFromApi(url)
14 | .then((res) => {
15 | setLoading(false);
16 | setData(res);
17 | })
18 | .catch((err) => {
19 | setLoading(false);
20 | setError("Something went wrong!");
21 | });
22 | }, [url]);
23 |
24 | return { data, loading, error };
25 | };
26 |
27 | export default useFetch;
28 |
--------------------------------------------------------------------------------
/src/index.scss:
--------------------------------------------------------------------------------
1 | :root {
2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
3 | font-size: 16px;
4 | line-height: 1;
5 | font-weight: 500;
6 |
7 | font-synthesis: none;
8 | text-rendering: optimizeLegibility;
9 | -webkit-font-smoothing: antialiased;
10 | -moz-osx-font-smoothing: grayscale;
11 | -webkit-text-size-adjust: 100%;
12 |
13 | --black: #04152d;
14 | --black2: #041226;
15 | --black3: #020c1b;
16 | --black-lighter: #1c4b91;
17 | --black-light: #173d77;
18 | --pink: #da2f68;
19 | --orange: #f89e00;
20 | --gradient: linear-gradient(98.37deg, #f89e00 0.99%, #da2f68 100%);
21 | }
22 |
23 | * {
24 | margin: 0;
25 | padding: 0;
26 | box-sizing: border-box;
27 | }
28 |
29 | body {
30 | background-color: var(--black);
31 | }
32 |
33 | ::-webkit-scrollbar {
34 | display: none;
35 | }
36 |
37 | .skeleton {
38 | position: relative;
39 | overflow: hidden;
40 | background-color: #0a2955;
41 | &::after {
42 | position: absolute;
43 | top: 0;
44 | right: 0;
45 | bottom: 0;
46 | left: 0;
47 | transform: translateX(-100%);
48 | background-image: linear-gradient(
49 | 90deg,
50 | rgba(#193763, 0) 0,
51 | rgba(#193763, 0.2) 20%,
52 | rgba(#193763, 0.5) 60%,
53 | rgba(#193763, 0)
54 | );
55 | animation: shimmer 2s infinite;
56 | content: "";
57 | }
58 |
59 | @keyframes shimmer {
60 | 100% {
61 | transform: translateX(100%);
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/main.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import App from "./App.jsx";
4 | import "./index.scss";
5 | import { Provider } from "react-redux";
6 | import { store } from "./store/store.js";
7 |
8 | ReactDOM.createRoot(document.getElementById("root")).render(
9 |
10 |
11 |
12 | );
13 |
--------------------------------------------------------------------------------
/src/mixin.scss:
--------------------------------------------------------------------------------
1 | @mixin sm {
2 | @media only screen and (min-width: 640px) {
3 | @content;
4 | }
5 | }
6 |
7 | @mixin md {
8 | @media only screen and (min-width: 768px) {
9 | @content;
10 | }
11 | }
12 |
13 | @mixin lg {
14 | @media only screen and (min-width: 1024px) {
15 | @content;
16 | }
17 | }
18 |
19 | @mixin xl {
20 | @media only screen and (min-width: 1280px) {
21 | @content;
22 | }
23 | }
24 |
25 | @mixin xxl {
26 | @media only screen and (min-width: 1536px) {
27 | @content;
28 | }
29 | }
30 |
31 | @mixin ellipsis($line: 2) {
32 | display: -webkit-box;
33 | -webkit-line-clamp: $line;
34 | -webkit-box-orient: vertical;
35 | overflow: hidden;
36 | text-overflow: ellipsis;
37 | }
--------------------------------------------------------------------------------
/src/pages/404/404.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import "./style.scss";
4 |
5 | import ContentWrapper from "../../components/contentWrapper/ContentWrapper";
6 |
7 | const PageNotFound = () => {
8 | return (
9 |
10 |
11 | 404
12 | Page not found!
13 |
14 |
15 | );
16 | };
17 |
18 | export default PageNotFound;
19 |
--------------------------------------------------------------------------------
/src/pages/404/style.scss:
--------------------------------------------------------------------------------
1 | .pageNotFound {
2 | height: 700px;
3 | padding-top: 200px;
4 | .contentWrapper {
5 | text-align: center;
6 | color: var(--black-light);
7 | display: flex;
8 | flex-direction: column;
9 | .bigText {
10 | font-size: 150px;
11 | font-weight: 700;
12 | }
13 | .smallText {
14 | font-size: 44px;
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/pages/details/Details.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import useFetch from "../../hooks/useFetch";
3 | import { useParams } from "react-router-dom";
4 | import DetailsBanner from "./detailsBanner/DetailsBanner";
5 | import Cast from "./cast/Cast";
6 | import VideosSection from "./videoSection/VideoSection";
7 | import Similar from "./carousels/Similar";
8 | import Recommendation from "./carousels/Recommendation";
9 | const Details = () => {
10 | const { mediaType, id } = useParams();
11 | const { data, loading } = useFetch(`/${mediaType}/${id}/videos`);
12 | const { data: credits, loading: creditsLoading } = useFetch(
13 | `/${mediaType}/${id}/credits`
14 | );
15 |
16 | const video = data?.results?.find((v) => {
17 | return v?.type === "Trailer";
18 | });
19 |
20 |
21 | // console.log(video?.[0]);
22 | // video={data?.results?.[0]}
23 | return (
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | );
32 | };
33 |
34 | export default Details;
35 |
--------------------------------------------------------------------------------
/src/pages/details/PlayBtn.jsx:
--------------------------------------------------------------------------------
1 | export const PlayBtn = () => {
2 | return (
3 |
36 | );
37 | };
38 |
--------------------------------------------------------------------------------
/src/pages/details/carousels/Recommendation.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import Carousel from "../../../components/carousel/Carousel";
4 | import useFetch from "../../../hooks/useFetch";
5 |
6 | const Recommendation = ({ mediaType, id }) => {
7 | const { data, loading, error } = useFetch(
8 | `/${mediaType}/${id}/recommendations`
9 | );
10 |
11 | return (
12 |
18 | );
19 | };
20 |
21 | export default Recommendation;
22 |
--------------------------------------------------------------------------------
/src/pages/details/carousels/Similar.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import Carousel from "../../../components/carousel/Carousel";
4 | import useFetch from "../../../hooks/useFetch";
5 |
6 | const Similar = ({ mediaType, id }) => {
7 | const { data, loading, error } = useFetch(`/${mediaType}/${id}/similar`);
8 |
9 | const title = mediaType === "tv" ? "Similar TV Shows" : "Similar Movies";
10 |
11 | return (
12 |
18 | );
19 | };
20 |
21 | export default Similar;
22 |
--------------------------------------------------------------------------------
/src/pages/details/cast/Cast.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useSelector } from "react-redux";
3 |
4 | import "./style.scss";
5 |
6 | import ContentWrapper from "../../../components/contentWrapper/ContentWrapper";
7 | import Img from "../../../components/lazyImg/Img";
8 | import avatar from "../../../assets/images/avatar.png";
9 |
10 | const Cast = ({ data, loading }) => {
11 | const { url } = useSelector((state) => state.home);
12 |
13 | const skeleton = () => {
14 | return (
15 |
20 | );
21 | };
22 | return (
23 |
24 |
25 | Top Cast
26 | {!loading ? (
27 |
28 | {data?.map((item) => {
29 | let imgUrl = item.profile_path
30 | ? url.profilr + item.profile_path
31 | : avatar;
32 | return (
33 |
34 |
35 |

36 |
37 |
{item.name}
38 |
{item.character}
39 |
40 | );
41 | })}
42 |
43 | ) : (
44 |
45 | {skeleton()}
46 | {skeleton()}
47 | {skeleton()}
48 | {skeleton()}
49 | {skeleton()}
50 | {skeleton()}
51 |
52 | )}
53 |
54 |
55 | );
56 | };
57 |
58 | export default Cast;
59 |
--------------------------------------------------------------------------------
/src/pages/details/cast/style.scss:
--------------------------------------------------------------------------------
1 | @import "../../../mixin.scss";
2 | .castSection {
3 | position: relative;
4 | margin-bottom: 50px;
5 | .sectionHeading {
6 | font-size: 24px;
7 | color: white;
8 | margin-bottom: 25px;
9 | }
10 | .listItems {
11 | display: flex;
12 | gap: 20px;
13 | overflow-y: hidden;
14 | margin-right: -20px;
15 | margin-left: -20px;
16 | padding: 0 20px;
17 | @include md {
18 | margin: 0;
19 | padding: 0;
20 | }
21 | .listItem {
22 | text-align: center;
23 | color: white;
24 | .profileImg {
25 | width: 125px;
26 | height: 125px;
27 | border-radius: 50%;
28 | overflow: hidden;
29 | margin-bottom: 15px;
30 | @include md {
31 | width: 175px;
32 | height: 175px;
33 | margin-bottom: 25px;
34 | }
35 | img {
36 | width: 100%;
37 | height: 100%;
38 | object-fit: cover;
39 | object-position: center top;
40 | display: block;
41 | }
42 | }
43 | .name {
44 | font-size: 14px;
45 | line-height: 20px;
46 | font-weight: 600;
47 | @include md {
48 | font-size: 18px;
49 | line-height: 24px;
50 | }
51 | }
52 | .character {
53 | font-size: 14px;
54 | line-height: 20px;
55 | opacity: 0.5;
56 | @include md {
57 | font-size: 16px;
58 | line-height: 24px;
59 | }
60 | }
61 | }
62 | }
63 |
64 | .castSkeleton {
65 | display: flex;
66 | gap: 20px;
67 | overflow-y: hidden;
68 | margin-right: -20px;
69 | margin-left: -20px;
70 | padding: 0 20px;
71 | @include md {
72 | margin: 0;
73 | padding: 0;
74 | }
75 | .skItem {
76 | .circle {
77 | width: 125px;
78 | height: 125px;
79 | border-radius: 50%;
80 | margin-bottom: 15px;
81 | @include md {
82 | width: 175px;
83 | height: 175px;
84 | margin-bottom: 25px;
85 | }
86 | }
87 | .row {
88 | width: 100%;
89 | height: 20px;
90 | border-radius: 10px;
91 | margin-bottom: 10px;
92 | }
93 | .row2 {
94 | width: 75%;
95 | height: 20px;
96 | border-radius: 10px;
97 | margin: 0 auto;
98 | }
99 | }
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/src/pages/details/detailsBanner/DetailsBanner.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { useParams } from "react-router-dom";
3 | import { useSelector } from "react-redux";
4 | import dayjs from "dayjs";
5 |
6 | import "./style.scss";
7 |
8 | import ContentWrapper from "../../../components/contentWrapper/ContentWrapper";
9 | import Genres from "../../../components/genres/Genres";
10 | import CircleRating from "../../../components/circleRating/CircleRating";
11 | import Img from "../../../components/lazyImg/Img";
12 | import PosterFallback from "../../../assets/images/no-poster.png";
13 | import useFetch from "../../../hooks/useFetch";
14 | import { PlayBtn } from "../PlayBtn";
15 | import VideoPopup from "../../../components/videoPopup/VideoPopup";
16 |
17 | const DetailsBanner = ({ video, crew }) => {
18 | const [show, setShow] = useState(false);
19 | const [videoId, setVideoId] = useState(null);
20 |
21 | const { mediaType, id } = useParams();
22 | const { data, loading } = useFetch(`/${mediaType}/${id}`);
23 |
24 | const { url } = useSelector((state) => state.home);
25 |
26 | const __genres = data?.genres?.map((g) => g.id);
27 |
28 | const director = crew?.filter((f) => f.job === "Director");
29 | const writer = crew?.filter(
30 | (f) => f.job === "Screenplay" || f.job === "Story" || f.job === "Writer"
31 | );
32 |
33 | const toHoursAndMinutes = (totalMinutes) => {
34 | const hours = Math.floor(totalMinutes / 60);
35 | const minutes = totalMinutes % 60;
36 | return `${hours}h${minutes > 0 ? ` ${minutes}m` : ""}`;
37 | };
38 |
39 | return (
40 |
41 | {!loading ? (
42 | <>
43 | {!!data && (
44 |
45 |
46 |

47 |
48 |
49 |
50 |
51 |
52 | {data.poster_path ? (
53 |

57 | ) : (
58 |

59 | )}
60 |
61 |
62 |
63 | {`${data.name || data.title}
64 | (${dayjs(data?.release).format("YYYY")})
65 | `}
66 |
67 |
{data?.tagline}
68 |
69 |
70 |
71 |
72 |
{
75 | setShow(true);
76 | setVideoId(video?.key);
77 | }}
78 | >
79 |
80 |
Watch Trailer
81 |
82 |
83 |
84 |
85 |
Overview
86 |
{data?.overview}
87 |
88 |
89 |
90 | {data.status && (
91 |
92 | Status:{" "}
93 | {data.status}
94 |
95 | )}
96 | {data.release_date && (
97 |
98 | Release Date:{" "}
99 |
100 | {dayjs(data.release_date).format("MMM D, YYYY")}
101 |
102 |
103 | )}
104 | {data.runtime && (
105 |
106 | Runtime:{" "}
107 |
108 | {toHoursAndMinutes(data.runtime)}
109 |
110 |
111 | )}
112 |
113 |
114 | {director?.length > 0 && (
115 |
116 | Director :
117 |
118 | {director?.map((d, i) => (
119 |
120 | {d.name}
121 | {director.length - 1 !== i && ", "}
122 |
123 | ))}
124 |
125 |
126 | )}
127 |
128 | {writer?.length > 0 && (
129 |
130 | Writer:
131 |
132 | {writer?.map((w, i) => (
133 |
134 | {w.name}
135 | {writer.length - 1 !== i && ", "}
136 |
137 | ))}
138 |
139 |
140 | )}
141 |
142 | {data?.created_by?.length > 0 && (
143 |
144 | Creator:
145 |
146 | {data?.created_by?.map((w, i) => (
147 |
148 | {w.name}
149 | {writer?.length - 1 === i && ", "}
150 |
151 | ))}
152 |
153 |
154 | )}
155 |
156 |
157 |
163 |
164 |
165 | )}
166 | >
167 | ) : (
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 | )}
183 |
184 | );
185 | };
186 |
187 | export default DetailsBanner;
188 |
--------------------------------------------------------------------------------
/src/pages/details/detailsBanner/style.scss:
--------------------------------------------------------------------------------
1 | @import "../../../mixin.scss";
2 | .detailsBanner {
3 | width: 100%;
4 | background-color: var(--black);
5 | padding-top: 100px;
6 | margin-bottom: 50px;
7 | @include md {
8 | margin-bottom: 0;
9 | padding-top: 120px;
10 | min-height: 700px;
11 | }
12 | .backdrop-img {
13 | width: 100%;
14 | height: 100%;
15 | position: absolute;
16 | top: 0;
17 | left: 0;
18 | opacity: 0.1;
19 | overflow: hidden;
20 | .lazy-load-image-background {
21 | width: 100%;
22 | height: 100%;
23 | img {
24 | width: 100%;
25 | height: 100%;
26 | object-fit: cover;
27 | object-position: center;
28 | }
29 | }
30 | }
31 | .opacity-layer {
32 | width: 100%;
33 | height: 250px;
34 | background: linear-gradient(180deg, rgba(4, 21, 45, 0) 0%, #04152d 79.17%);
35 | position: absolute;
36 | bottom: 0;
37 | left: 0;
38 | }
39 |
40 | .content {
41 | display: flex;
42 | position: relative;
43 | flex-direction: column;
44 | gap: 25px;
45 | @include md {
46 | gap: 50px;
47 | flex-direction: row;
48 | }
49 | .left {
50 | flex-shrink: 0;
51 | .posterImg {
52 | width: 100%;
53 |
54 | display: block;
55 | border-radius: 12px;
56 | @include md {
57 | max-width: 350px;
58 | }
59 | }
60 | }
61 | .right {
62 | color: white;
63 | .title {
64 | font-size: 28px;
65 | line-height: 40px;
66 | @include md {
67 | font-size: 34px;
68 | line-height: 44px;
69 | }
70 | }
71 | .subtitle {
72 | font-size: 16px;
73 | line-height: 24px;
74 | margin-bottom: 15px;
75 | font-style: italic;
76 | opacity: 0.5;
77 | @include md {
78 | font-size: 20px;
79 | line-height: 28px;
80 | }
81 | }
82 | .genres {
83 | margin-bottom: 25px;
84 | flex-flow: row wrap;
85 | }
86 | .overview {
87 | margin-bottom: 25px;
88 | .heading {
89 | font-size: 24px;
90 | margin-bottom: 10px;
91 | }
92 | .description {
93 | line-height: 24px;
94 | @include md {
95 | padding-right: 100px;
96 | }
97 | }
98 | }
99 | .circleRating {
100 | max-width: 70px;
101 | background-color: var(--black2);
102 | @include md {
103 | max-width: 90px;
104 | }
105 | .CircularProgressbar-text {
106 | fill: white;
107 | }
108 | }
109 | .playbtn {
110 | display: flex;
111 | align-items: center;
112 | gap: 20px;
113 | cursor: pointer;
114 | svg {
115 | width: 60px;
116 | @include md {
117 | width: 80px;
118 | }
119 | }
120 | .text {
121 | font-size: 20px;
122 | transition: all 0.7s ease-in-out;
123 | }
124 | .triangle {
125 | stroke-dasharray: 240;
126 | stroke-dashoffset: 480;
127 | stroke: white;
128 | transform: translateY(0);
129 | transition: all 0.7s ease-in-out;
130 | }
131 | .circle {
132 | stroke: white;
133 | stroke-dasharray: 650;
134 | stroke-dashoffset: 1300;
135 | transition: all 0.5s ease-in-out;
136 | }
137 | &:hover {
138 | .text {
139 | color: var(--pink);
140 | }
141 | .triangle {
142 | stroke-dashoffset: 0;
143 | opacity: 1;
144 | stroke: var(--pink);
145 | animation: trailorPlay 0.7s ease-in-out;
146 | }
147 | .circle {
148 | stroke-dashoffset: 0;
149 | stroke: var(--pink);
150 | }
151 | }
152 | }
153 | .row {
154 | display: flex;
155 | align-items: center;
156 | gap: 25px;
157 | margin-bottom: 25px;
158 | }
159 |
160 | .info {
161 | border-bottom: 1px solid rgba(255, 255, 255, 0.1);
162 | padding: 15px 0;
163 | display: flex;
164 | .infoItem {
165 | margin-right: 10px;
166 | display: flex;
167 | flex-flow: row wrap;
168 | }
169 | .text {
170 | margin-right: 10px;
171 | opacity: 0.5;
172 | line-height: 24px;
173 | &.bold {
174 | font-weight: 600;
175 | opacity: 1;
176 | }
177 | }
178 | }
179 | }
180 | }
181 |
182 | .detailsBannerSkeleton {
183 | display: flex;
184 | position: relative;
185 | flex-direction: column;
186 | gap: 25px;
187 | @include md {
188 | gap: 50px;
189 | flex-direction: row;
190 | }
191 | .contentWrapper {
192 | display: flex;
193 | gap: 50px;
194 | }
195 | .left {
196 | flex-shrink: 0;
197 | width: 100%;
198 | display: block;
199 | border-radius: 12px;
200 | aspect-ratio: 1/1.5;
201 | @include md {
202 | max-width: 350px;
203 | }
204 | }
205 | .right {
206 | width: 100%;
207 | .row {
208 | width: 100%;
209 | height: 25px;
210 | margin-bottom: 20px;
211 | border-radius: 50px;
212 | &:nth-child(2) {
213 | width: 75%;
214 | margin-bottom: 50px;
215 | }
216 | &:nth-child(5) {
217 | width: 50%;
218 | margin-bottom: 50px;
219 | }
220 | }
221 | }
222 | }
223 | }
224 |
--------------------------------------------------------------------------------
/src/pages/details/style.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mukeshpandey9/Movix_App/188cb0f44523d5d6a7ce52382e92f29107101ce6/src/pages/details/style.scss
--------------------------------------------------------------------------------
/src/pages/details/videoSection/VideoSection.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 |
3 | import "./style.scss";
4 |
5 | import ContentWrapper from "../../../components/contentWrapper/ContentWrapper";
6 |
7 | import VideoPopup from "../../../components/videoPopup/VideoPopup";
8 | import Img from "../../../components/lazyImg/Img";
9 | import { PlayBtn } from "../PlayBtn";
10 |
11 | const VideosSection = ({ data, loading }) => {
12 | const [show, setShow] = useState(false);
13 | const [videoId, setVideoId] = useState(null);
14 |
15 | const loadingSkeleton = () => {
16 | return (
17 |
22 | );
23 | };
24 |
25 | return (
26 |
27 |
28 | Official Videos
29 | {!loading ? (
30 |
31 | {data?.results?.map((video) => (
32 |
{
36 | setVideoId(video?.key);
37 | setShow(true);
38 | }}
39 | >
40 |
41 |

44 |
45 |
46 |
{video.name}
47 |
48 | ))}
49 |
50 | ) : (
51 |
52 | {loadingSkeleton()}
53 | {loadingSkeleton()}
54 | {loadingSkeleton()}
55 | {loadingSkeleton()}
56 |
57 | )}
58 |
59 |
65 |
66 | );
67 | };
68 |
69 | export default VideosSection;
70 |
--------------------------------------------------------------------------------
/src/pages/details/videoSection/style.scss:
--------------------------------------------------------------------------------
1 | @import "../../../mixin.scss";
2 | .videosSection {
3 | position: relative;
4 | margin-bottom: 50px;
5 | .sectionHeading {
6 | font-size: 24px;
7 | color: white;
8 | margin-bottom: 25px;
9 | }
10 | .videos {
11 | display: flex;
12 | gap: 10px;
13 | overflow-x: auto;
14 | margin-right: -20px;
15 | margin-left: -20px;
16 | padding: 0 20px;
17 | @include md {
18 | gap: 20px;
19 | margin: 0;
20 | padding: 0;
21 | }
22 | .videoItem {
23 | width: 150px;
24 | flex-shrink: 0;
25 | @include md {
26 | width: 25%;
27 | }
28 | cursor: pointer;
29 | .videoThumbnail {
30 | margin-bottom: 15px;
31 | position: relative;
32 | img {
33 | width: 100%;
34 | display: block;
35 | border-radius: 12px;
36 | transition: all 0.7s ease-in-out;
37 | }
38 | svg {
39 | position: absolute;
40 | top: 50%;
41 | left: 50%;
42 | transform: translate(-50%, -50%);
43 | width: 50px;
44 | height: 50px;
45 | }
46 | .triangle {
47 | stroke-dasharray: 240;
48 | stroke-dashoffset: 480;
49 | stroke: white;
50 | transform: translateY(0);
51 | transition: all 0.7s ease-in-out;
52 | }
53 | .circle {
54 | stroke: white;
55 | stroke-dasharray: 650;
56 | stroke-dashoffset: 1300;
57 | transition: all 0.5s ease-in-out;
58 | }
59 | &:hover {
60 | img {
61 | opacity: 0.5;
62 | }
63 | .triangle {
64 | stroke-dashoffset: 0;
65 | opacity: 1;
66 | stroke: var(--pink);
67 | animation: trailorPlay 0.7s ease-in-out;
68 | }
69 | .circle {
70 | stroke-dashoffset: 0;
71 | stroke: var(--pink);
72 | }
73 | }
74 | }
75 | .videoTitle {
76 | color: white;
77 | font-size: 14px;
78 | line-height: 20px;
79 | @include md {
80 | font-size: 16px;
81 | line-height: 24px;
82 | }
83 | }
84 | }
85 | }
86 |
87 | .videoSkeleton {
88 | display: flex;
89 | gap: 10px;
90 | overflow-x: auto;
91 | margin-right: -20px;
92 | margin-left: -20px;
93 | padding: 0 20px;
94 | @include md {
95 | gap: 20px;
96 | margin: 0;
97 | padding: 0;
98 | }
99 | .skItem {
100 | width: 150px;
101 | flex-shrink: 0;
102 | @include md {
103 | width: 25%;
104 | }
105 | .thumb {
106 | width: 100%;
107 | aspect-ratio: 16 / 9;
108 | border-radius: 12px;
109 | margin-bottom: 10px;
110 | }
111 | .row {
112 | height: 20px;
113 | width: 100%;
114 | border-radius: 10px;
115 | margin-bottom: 10px;
116 | }
117 | .row2 {
118 | height: 20px;
119 | width: 75%;
120 | border-radius: 10px;
121 | }
122 | }
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/src/pages/explore/Explore.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { useParams } from "react-router-dom";
3 | import InfiniteScroll from "react-infinite-scroll-component";
4 | import Select from "react-select";
5 |
6 | import "./style.scss";
7 |
8 | import useFetch from "../../hooks/useFetch";
9 | import { fetchDataFromApi } from "../../utils/api";
10 | import ContentWrapper from "../../components/contentWrapper/ContentWrapper";
11 | import MovieCard from "../../components/movieCard/MovieCard";
12 | import Spinner from "../../components/spinner/Spinner";
13 |
14 | let filters = {};
15 |
16 | const sortbyData = [
17 | { value: "popularity.desc", label: "Popularity Descending" },
18 | { value: "popularity.asc", label: "Popularity Ascending" },
19 | { value: "vote_average.desc", label: "Rating Descending" },
20 | { value: "vote_average.asc", label: "Rating Ascending" },
21 | {
22 | value: "primary_release_date.desc",
23 | label: "Release Date Descending",
24 | },
25 | { value: "primary_release_date.asc", label: "Release Date Ascending" },
26 | { value: "original_title.asc", label: "Title (A-Z)" },
27 | ];
28 |
29 | const Explore = () => {
30 | const [data, setData] = useState(null);
31 | const [pageNum, setPageNum] = useState(1);
32 | const [loading, setLoading] = useState(false);
33 | const [genre, setGenre] = useState(null);
34 | const [sortby, setSortby] = useState(null);
35 | const { mediaType } = useParams();
36 |
37 | const { data: genresData } = useFetch(`/genre/${mediaType}/list`);
38 |
39 | const fetchInitialData = () => {
40 | setLoading(true);
41 | fetchDataFromApi(`/discover/${mediaType}`, filters).then((res) => {
42 | setData(res);
43 | setPageNum((prev) => prev + 1);
44 | setLoading(false);
45 | });
46 | };
47 |
48 | const fetchNextPageData = () => {
49 | fetchDataFromApi(`/discover/${mediaType}?page=${pageNum}`, filters).then(
50 | (res) => {
51 | if (data?.results) {
52 | setData({
53 | ...data,
54 | results: [...data?.results, ...res.results],
55 | });
56 | } else {
57 | setData(res);
58 | }
59 | setPageNum((prev) => prev + 1);
60 | }
61 | );
62 | };
63 |
64 | useEffect(() => {
65 | filters = {};
66 | setData(null);
67 | setPageNum(1);
68 | setSortby(null);
69 | setGenre(null);
70 | fetchInitialData();
71 | }, [mediaType]);
72 |
73 | const onChange = (selectedItems, action) => {
74 | if (action.name === "sortby") {
75 | setSortby(selectedItems);
76 | if (action.action !== "clear") {
77 | filters.sort_by = selectedItems.value;
78 | } else {
79 | delete filters.sort_by;
80 | }
81 | }
82 |
83 | if (action.name === "genres") {
84 | setGenre(selectedItems);
85 | if (action.action !== "clear") {
86 | let genreId = selectedItems.map((g) => g.id);
87 | genreId = JSON.stringify(genreId).slice(1, -1);
88 | filters.with_genres = genreId;
89 | } else {
90 | delete filters.with_genres;
91 | }
92 | }
93 |
94 | setPageNum(1);
95 | fetchInitialData();
96 | };
97 |
98 | return (
99 |
100 |
101 |
102 |
103 | {mediaType === "tv" ? "Explore TV Shows" : "Explore Movies"}
104 |
105 |
106 |
130 |
131 | {loading && }
132 | {!loading && (
133 | <>
134 | {data?.results?.length > 0 ? (
135 | }
141 | >
142 | {data?.results?.map((item, index) => {
143 | if (item.media_type === "person") return;
144 | return (
145 |
146 | );
147 | })}
148 |
149 | ) : (
150 | Sorry, Results not found!
151 | )}
152 | >
153 | )}
154 |
155 |
156 | );
157 | };
158 |
159 | export default Explore;
160 |
--------------------------------------------------------------------------------
/src/pages/explore/style.scss:
--------------------------------------------------------------------------------
1 | @import "../../mixin.scss";
2 | .explorePage {
3 | min-height: 700px;
4 | padding-top: 100px;
5 | .resultNotFound {
6 | font-size: 24px;
7 | color: var(--black-light);
8 | }
9 | .pageHeader {
10 | display: flex;
11 | justify-content: space-between;
12 | margin-bottom: 25px;
13 | flex-direction: column;
14 | @include md {
15 | flex-direction: row;
16 | }
17 | }
18 | .pageTitle {
19 | font-size: 24px;
20 | line-height: 34px;
21 | color: white;
22 | margin-bottom: 20px;
23 | @include md {
24 | margin-bottom: 0;
25 | }
26 | }
27 | .filters {
28 | display: flex;
29 | gap: 10px;
30 | flex-direction: column;
31 | @include md {
32 | flex-direction: row;
33 | }
34 | .react-select-container {
35 | &.genresDD {
36 | width: 100%;
37 | @include md {
38 | max-width: 500px;
39 | min-width: 250px;
40 | }
41 | }
42 | &.sortbyDD {
43 | width: 100%;
44 | flex-shrink: 0;
45 | @include md {
46 | width: 250px;
47 | }
48 | }
49 | .react-select__control {
50 | border: 0;
51 | outline: 0;
52 | box-shadow: none;
53 | background-color: var(--black-light);
54 | border-radius: 20px;
55 | .react-select__value-container {
56 | .react-select__placeholder,
57 | .react-select__input-container {
58 | color: white;
59 | margin: 0 10px;
60 | }
61 | }
62 | .react-select__single-value {
63 | color: white;
64 | }
65 | .react-select__multi-value {
66 | background-color: var(--black3);
67 | border-radius: 10px;
68 | .react-select__multi-value__label {
69 | color: white;
70 | }
71 | .react-select__multi-value__remove {
72 | background-color: transparent;
73 | color: white;
74 | cursor: pointer;
75 | &:hover {
76 | color: var(--black-lighter);
77 | }
78 | }
79 | }
80 | }
81 | .react-select__menu {
82 | top: 40px;
83 | margin: 0;
84 | padding: 0;
85 | }
86 | }
87 | }
88 | .content {
89 | display: flex;
90 | flex-flow: row wrap;
91 | gap: 10px;
92 | margin-bottom: 50px;
93 | @include md {
94 | gap: 20px;
95 | }
96 | .movieCard {
97 | .posterBlock {
98 | margin-bottom: 30px;
99 | }
100 | }
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/pages/home/Home.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "./style.scss";
3 | import HeroBanner from "./heroBanner/HeroBanner";
4 | import Trending from "./trending/Trending";
5 | import Popular from "./popular/Popular";
6 | import TopRated from "./topRated/TopRated";
7 | const Home = () => {
8 | return (
9 | <>
10 |
16 | >
17 | );
18 | };
19 |
20 | export default Home;
21 |
--------------------------------------------------------------------------------
/src/pages/home/heroBanner/HeroBanner.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import "./style.scss";
3 | import { useNavigate } from "react-router-dom";
4 | import useFetch from "../../../hooks/useFetch";
5 | import { useSelector } from "react-redux";
6 | import Img from "../../../components/lazyImg/Img";
7 | import ContentWrapper from "../../../components/contentWrapper/ContentWrapper";
8 | const HeroBanner = () => {
9 | const navigate = useNavigate();
10 | const [background, setbackground] = useState("");
11 | const [query, setquery] = useState("");
12 | const { url } = useSelector((state) => state.home);
13 | const { data, loading } = useFetch("/movie/upcoming");
14 |
15 | useEffect(() => {
16 | const bg =
17 | url.backdrop +
18 | data?.results[Math.floor(Math.random() * 20)]?.backdrop_path;
19 | setbackground(bg);
20 | }, [data]);
21 |
22 | const searchQueryHandler = (event) => {
23 | if (event.key === "Enter" && query.length > 0) {
24 | navigate(`/search/${query}`);
25 | }
26 | };
27 | return (
28 | <>
29 |
30 | {!loading && (
31 |
32 |

33 |
34 | )}
35 |
36 |
37 |
38 |
39 |
Welcome
40 |
41 | Millions of movies, TV shows and people to discover.Explore now
42 |
43 |
44 | setquery(e.target.value)}
48 | onKeyUp={searchQueryHandler}
49 | />
50 |
53 |
54 |
55 |
56 |
57 | >
58 | );
59 | };
60 |
61 | export default HeroBanner;
62 |
--------------------------------------------------------------------------------
/src/pages/home/heroBanner/style.scss:
--------------------------------------------------------------------------------
1 | @import "../../../mixin.scss";
2 | .heroBanner {
3 | width: 100%;
4 | height: 450px;
5 | background-color: var(--black);
6 | display: flex;
7 | align-items: center;
8 | position: relative;
9 | @include md {
10 | height: 650px;
11 | }
12 |
13 | .backdrop-img {
14 | width: 100%;
15 | height: 100%;
16 | position: absolute;
17 | top: 0;
18 | left: 0;
19 | opacity: 0.5;
20 | overflow: hidden;
21 |
22 | .lazy-load-image-background {
23 | width: 100%;
24 | height: 100%;
25 | img {
26 | width: 100%;
27 | height: 100%;
28 | object-fit: cover;
29 | object-position: center;
30 | }
31 | }
32 | }
33 |
34 | .opacity-layer {
35 | width: 100%;
36 | height: 270px;
37 | background: linear-gradient(181deg, rgba(4, 21, 45, 0) 0%, #04152d 79.17%);
38 | position: absolute;
39 | bottom: 0;
40 | left: 0;
41 | }
42 |
43 | .heroBannerContent {
44 | display: flex;
45 | flex-direction: column;
46 | align-items: center;
47 | color: white;
48 | text-align: center;
49 | position: relative;
50 | max-width: 800px;
51 | margin: 0 auto;
52 | .title {
53 | font-size: 50px;
54 | font-weight: 700;
55 | margin-bottom: 10px;
56 | @include md {
57 | margin-bottom: 0;
58 | font-size: 90px;
59 | }
60 | }
61 |
62 | .subTitle {
63 | font-size: 18px;
64 | font-weight: 500;
65 | margin-bottom: 40px;
66 | @include md {
67 | font-size: 24px;
68 | }
69 | }
70 |
71 | .searchInput {
72 | display: flex;
73 | align-items: center;
74 | width: 100%;
75 | input {
76 | width: calc(100% - 100px);
77 | height: 50px;
78 | background-color: white;
79 | outline: 0;
80 | border: 0;
81 | padding: 0 20px;
82 | border-radius: 30px 0 0 30px;
83 | @include md {
84 | width: calc(100% - 150px);
85 | height: 60px;
86 | font-size: 20px;
87 | padding: 0 30px;
88 | }
89 | }
90 |
91 | button {
92 | width: 100px;
93 | height: 50px;
94 | background: var(--gradient);
95 | color: white;
96 | outline: 0;
97 | border: 0;
98 | border-radius: 0px 30px 30px 0px;
99 | font-size: 16px;
100 | cursor: pointer;
101 | @include md {
102 | width: 150px;
103 | height: 60px;
104 | font-size: 18px;
105 | }
106 | }
107 | }
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/src/pages/home/popular/Popular.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import ContentWrapper from "../../../components/contentWrapper/ContentWrapper";
3 | import SwitchTabs from "../../../components/switchTabs/SwitchTabs";
4 | import useFetch from "../../../hooks/useFetch";
5 | import Carousel from "../../../components/carousel/Carousel";
6 |
7 | const Popular = () => {
8 | const [endPoint, setEndPoint] = useState("movie");
9 | const { data, loading } = useFetch(`/${endPoint}/popular`);
10 | const onTabChange = (tab) => {
11 | setEndPoint(tab === "Movies" ? "movie" : "tv");
12 | };
13 | return (
14 | <>
15 |
16 |
17 | Popular
18 |
19 |
20 |
21 |
22 | >
23 | );
24 | };
25 |
26 | export default Popular;
27 |
--------------------------------------------------------------------------------
/src/pages/home/style.scss:
--------------------------------------------------------------------------------
1 | .carouselSection {
2 | position: relative;
3 | margin-bottom: 70px;
4 | & > .contentWrapper {
5 | display: flex;
6 | align-items: center;
7 | justify-content: space-between;
8 | margin-bottom: 20px;
9 | }
10 |
11 | .carouselTitle {
12 | font-size: 24px;
13 | color: white;
14 | font-weight: normal;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/pages/home/topRated/TopRated.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import ContentWrapper from "../../../components/contentWrapper/ContentWrapper";
3 | import SwitchTabs from "../../../components/switchTabs/SwitchTabs";
4 | import useFetch from "../../../hooks/useFetch";
5 | import Carousel from "../../../components/carousel/Carousel";
6 |
7 | const TopRated = () => {
8 | const [endPoint, setEndPoint] = useState("movie");
9 | const { data, loading } = useFetch(`/${endPoint}/top_rated`);
10 | const onTabChange = (tab) => {
11 | console.log(tab);
12 | setEndPoint(tab === "Movies" ? "movie" : "tv");
13 | };
14 | return (
15 | <>
16 |
17 |
18 | TopRated
19 |
20 |
21 |
22 |
23 | >
24 | );
25 | };
26 |
27 | export default TopRated;
28 |
--------------------------------------------------------------------------------
/src/pages/home/trending/Trending.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import ContentWrapper from "../../../components/contentWrapper/ContentWrapper";
3 | import SwitchTabs from "../../../components/switchTabs/SwitchTabs";
4 | import useFetch from "../../../hooks/useFetch";
5 | import Carousel from "../../../components/carousel/Carousel";
6 |
7 | const Trending = () => {
8 | const [endPoint, setEndPoint] = useState("day");
9 | const { data, loading } = useFetch(`/trending/all/${endPoint}`);
10 | const onTabChange = (tab) => {
11 | console.log(tab);
12 | setEndPoint(tab === "Day" ? "day" : "week");
13 | };
14 | return (
15 | <>
16 |
17 |
18 | Trending
19 |
20 |
21 |
22 |
23 | >
24 | );
25 | };
26 |
27 | export default Trending;
28 |
--------------------------------------------------------------------------------
/src/pages/searchResult/SearchResult.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { useParams } from "react-router-dom";
3 | import InfiniteScroll from "react-infinite-scroll-component";
4 | import "./style.scss";
5 |
6 | import { fetchDataFromApi } from "../../utils/api";
7 | import ContentWrapper from "../../components/contentWrapper/ContentWrapper";
8 | import MovieCard from "../../components/movieCard/MovieCard";
9 | import Spinner from "../../components/spinner/Spinner";
10 | import noResults from "../../assets/images/no-results.png";
11 |
12 | const SearchResult = () => {
13 | const [data, setData] = useState(null);
14 | const [pageNum, setPageNum] = useState(1);
15 | const [loading, setLoading] = useState(false);
16 | const { query } = useParams();
17 |
18 | const fetchInitialData = () => {
19 | setLoading(true);
20 |
21 | fetchDataFromApi(`/search/multi?query=${query}&page=${pageNum}`).then(
22 | (res) => {
23 | setData(res);
24 | setPageNum((prev) => prev + 1);
25 | setLoading(false);
26 | }
27 | );
28 | };
29 |
30 | const fetchNextPageData = () => {
31 | setLoading(true);
32 | fetchDataFromApi(`/search/multi?query=${query}&page=${pageNum}`).then(
33 | (res) => {
34 | if (data?.results) {
35 | setData({ ...data, results: [...data?.results, ...res.results] });
36 | } else {
37 | setData(res);
38 | }
39 |
40 | setPageNum((prev) => prev + 1);
41 | }
42 | );
43 | };
44 |
45 | useEffect(() => {
46 | setPageNum(1);
47 | fetchInitialData();
48 | }, [query]);
49 |
50 | return (
51 |
52 | {loading &&
}
53 | {!loading && (
54 |
55 | {data?.results?.length > 0 ? (
56 | <>
57 |
58 | {`Search ${
59 | data?.total_results > 1 ? "results" : "result"
60 | } of '${query}'`}
61 |
62 |
63 | }
69 | >
70 | {data?.results?.map((item, index) => {
71 | if (item.media_type === "person") return;
72 | return (
73 |
74 | );
75 | })}
76 |
77 | >
78 | ) : (
79 | Sorry, Results Not Found
80 | )}
81 |
82 | )}
83 |
84 | );
85 | };
86 |
87 | export default SearchResult;
88 |
--------------------------------------------------------------------------------
/src/pages/searchResult/style.scss:
--------------------------------------------------------------------------------
1 | @import "../../mixin.scss";
2 | .searchResultsPage {
3 | min-height: 700px;
4 | padding-top: 100px;
5 | .resultNotFound {
6 | font-size: 24px;
7 | color: var(--black-light);
8 | }
9 | .pageTitle {
10 | font-size: 24px;
11 | line-height: 34px;
12 | color: white;
13 | margin-bottom: 25px;
14 | }
15 | .content {
16 | display: flex;
17 | flex-flow: row wrap;
18 | gap: 10px;
19 | margin-bottom: 50px;
20 | @include md {
21 | gap: 20px;
22 | }
23 | .movieCard {
24 | .posterBlock {
25 | margin-bottom: 20px;
26 | }
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/store/homeSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 |
3 | export const homeSlice = createSlice({
4 | name: "home",
5 | initialState: {
6 | url: {},
7 | genres: {},
8 | },
9 | reducers: {
10 | getApiConfiguration: (state, action) => {
11 | state.url = action.payload;
12 | },
13 | getGenres: (state, action) => {
14 | state.genres = action.payload;
15 | },
16 | },
17 | });
18 |
19 | // Action creators are generated for each case reducer function
20 | export const { getApiConfiguration, getGenres } = homeSlice.actions;
21 |
22 | export default homeSlice.reducer;
23 |
--------------------------------------------------------------------------------
/src/store/store.js:
--------------------------------------------------------------------------------
1 | import { configureStore } from "@reduxjs/toolkit";
2 | import homeSlice from "./homeSlice";
3 |
4 | export const store = configureStore({
5 | reducer: {
6 | home: homeSlice,
7 | },
8 | });
9 |
--------------------------------------------------------------------------------
/src/utils/api.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | const BASE_URL = "https://api.themoviedb.org/3";
4 |
5 | const TMDB_TOKEN = import.meta.env.VITE_APP_TMDB_TOKEN;
6 |
7 | const headers = {
8 | Authorization: "bearer " + TMDB_TOKEN,
9 | };
10 |
11 | export const fetchDataFromApi = async (url, params) => {
12 | try {
13 | const { data } = await axios.get(BASE_URL + url, {
14 | headers,
15 | params,
16 | });
17 | return data;
18 | } catch (error) {
19 | console.log(error.message);
20 | }
21 | };
22 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import react from "@vitejs/plugin-react";
3 |
4 | // https://vitejs.dev/config
5 | export default defineConfig({
6 | plugins: [react()],
7 | server: {
8 | host: true,
9 | },
10 | });
11 |
--------------------------------------------------------------------------------