├── server ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .env_sample ├── Dockerfile.server ├── __test__ │ ├── helpers │ │ └── createUser.js │ └── db.js ├── routes │ ├── user.js │ ├── collection.js │ └── tmdb.js ├── server.js ├── models │ ├── collection.js │ └── user.js ├── middleware │ └── userAuthorization.js ├── app.js ├── controllers │ ├── userController.js │ └── collectionController.js ├── package.json ├── eslint.config.mjs └── .eslintcache ├── client ├── .env_sample ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── maskable-icon.png │ ├── manifest.json │ ├── index.html │ └── offline.html ├── .prettierrc ├── .prettierignore ├── postcss.config.js ├── src │ ├── assets │ │ ├── LoadingCard.svg │ │ ├── LoadingHero.svg │ │ ├── EmptyHero.svg │ │ ├── account.svg │ │ ├── EmptyCard.svg │ │ └── userHero.svg │ ├── loaders │ │ ├── Test.js │ │ ├── screens │ │ │ ├── ListingScreen.js │ │ │ ├── MainScreen.js │ │ │ ├── UserDetailsScreen.js │ │ │ └── DetailsScreen.js │ │ ├── partials │ │ │ ├── Tabs.js │ │ │ ├── Hero.js │ │ │ ├── Carousel.js │ │ │ ├── Details.js │ │ │ ├── Footer.js │ │ │ ├── UserDetails.js │ │ │ └── Cards.js │ │ └── LazyImage.js │ ├── components │ │ ├── general │ │ │ ├── ScrollToTop.js │ │ │ ├── BackNavigation.js │ │ │ ├── Category.js │ │ │ ├── Navbar.js │ │ │ ├── Search.js │ │ │ └── Footer.js │ │ ├── user │ │ │ ├── UserPerson.js │ │ │ ├── UserShows.js │ │ │ ├── UserMovies.js │ │ │ ├── UserListing.js │ │ │ ├── UserProfile.js │ │ │ └── UserTabs.js │ │ ├── rating │ │ │ ├── CardRating.js │ │ │ └── HeroRating.js │ │ ├── artist │ │ │ ├── department │ │ │ │ ├── WritingDepartment.js │ │ │ │ ├── ProductionDepartment.js │ │ │ │ ├── DirectingDepartment.js │ │ │ │ └── ActingDepartment.js │ │ │ ├── ArtistDetails.js │ │ │ ├── ArtistCredits.js │ │ │ ├── ArtistPhotos.js │ │ │ └── ArtistSummary.js │ │ └── details │ │ │ ├── DetailsEpisodes.js │ │ │ └── DetailsVideos.js │ ├── index.js │ ├── redux │ │ ├── store.js │ │ ├── userSlice.js │ │ ├── layoutSlice.js │ │ ├── genreSlice.js │ │ ├── userApi.js │ │ └── tmdbApi.js │ ├── hooks │ │ └── useUniqueData.js │ ├── pages │ │ ├── Account.js │ │ ├── details │ │ │ ├── PersonDetails.js │ │ │ ├── MovieDetails.js │ │ │ └── ShowDetails.js │ │ ├── Home.js │ │ ├── shows │ │ │ ├── TrendingShows.js │ │ │ ├── PopularShows.js │ │ │ ├── OnTheAirShows.js │ │ │ ├── TopRatedShows.js │ │ │ ├── AiringTodayShows.js │ │ │ ├── ShowGenre.js │ │ │ └── Shows.js │ │ ├── movies │ │ │ ├── TrendingMovies.js │ │ │ ├── PopularMovies.js │ │ │ ├── NowPlayingMovies.js │ │ │ ├── TopRatedMovies.js │ │ │ ├── UpcomingMovies.js │ │ │ ├── MovieGenre.js │ │ │ └── Movies.js │ │ └── SearchResults.js │ ├── index.css │ └── service-worker.js ├── .vscode │ └── settings.json ├── Dockerfile.client ├── cypress │ ├── helpers │ │ ├── performLogin.js │ │ ├── checkHero.js │ │ ├── checkNavigation.js │ │ ├── checkHeroTypography.js │ │ ├── checkCarousel.js │ │ └── checkFooter.js │ ├── support │ │ ├── e2e.js │ │ └── commands.js │ └── e2e │ │ ├── home │ │ └── home.cy.js │ │ ├── shows │ │ └── shows.cy.js │ │ ├── movies │ │ └── movies.cy.js │ │ ├── information │ │ ├── actions.cy.js │ │ └── information.cy.js │ │ └── artistInformation │ │ └── artistInformation.cy.js ├── cypress.config.js ├── tailwind.config.js ├── eslint.config.mjs └── package.json ├── .husky └── pre-commit ├── .gitignore ├── docker-compose.yml ├── .github └── workflows │ ├── deployment-client.yml │ ├── deployment-server.yml │ └── testing.yml ├── LICENSE ├── package.json └── README.md /server/.gitignore: -------------------------------------------------------------------------------- 1 | .env -------------------------------------------------------------------------------- /client/.env_sample: -------------------------------------------------------------------------------- 1 | REACT_APP_BACKEND_URL=YOUR_BACKEND_URL -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run lint 5 | 6 | -------------------------------------------------------------------------------- /server/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | .env_sample 4 | .gitignore 5 | .prettierrc 6 | -------------------------------------------------------------------------------- /client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloukey/myMovies/HEAD/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloukey/myMovies/HEAD/client/public/logo192.png -------------------------------------------------------------------------------- /client/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloukey/myMovies/HEAD/client/public/logo512.png -------------------------------------------------------------------------------- /client/public/maskable-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloukey/myMovies/HEAD/client/public/maskable-icon.png -------------------------------------------------------------------------------- /client/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": false 6 | } 7 | -------------------------------------------------------------------------------- /client/.prettierignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | node_modules 3 | public 4 | .prettierrc 5 | postcss.config.js 6 | tailwind.config.js 7 | src/assets -------------------------------------------------------------------------------- /client/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /server/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": false 6 | } 7 | -------------------------------------------------------------------------------- /client/src/assets/LoadingCard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/assets/LoadingHero.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "emmet.preferences": { 3 | "emmet.includeLanguages": { 4 | "javascript": "javascriptreact" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /server/.env_sample: -------------------------------------------------------------------------------- 1 | PORT=PORT_VALUE 2 | MONGODB_URI=YOUR_MONGODB_URI 3 | JWT_SECRET_KEY=YOUR_JWT_SECRET_KEY 4 | BASE_URL=https://api.themoviedb.org/3/ 5 | API_KEY=YOUR_TMDB_API_KEY -------------------------------------------------------------------------------- /client/Dockerfile.client: -------------------------------------------------------------------------------- 1 | FROM node:alpine 2 | 3 | WORKDIR /app 4 | 5 | COPY package.json . 6 | 7 | RUN npm install 8 | 9 | COPY . . 10 | 11 | EXPOSE 3000 12 | 13 | CMD ["npm", "start"] -------------------------------------------------------------------------------- /server/Dockerfile.server: -------------------------------------------------------------------------------- 1 | FROM node:alpine 2 | 3 | WORKDIR /app 4 | 5 | COPY package.json . 6 | 7 | RUN npm install 8 | 9 | COPY . . 10 | 11 | EXPOSE 8000 12 | 13 | CMD ["node", "server.js"] -------------------------------------------------------------------------------- /client/src/loaders/Test.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | const Test = () => { 3 | 4 | useEffect(() => {}, []); 5 | 6 | return
; 7 | }; 8 | export default Test; 9 | -------------------------------------------------------------------------------- /client/cypress/helpers/performLogin.js: -------------------------------------------------------------------------------- 1 | const performLogin = (email, password) => { 2 | cy.getCypress("account-card-email-input").type(email); 3 | cy.getCypress("account-card-password-input").type(password); 4 | cy.getCypress("account-card-login-button").click(); 5 | }; 6 | 7 | export { performLogin }; 8 | -------------------------------------------------------------------------------- /server/__test__/helpers/createUser.js: -------------------------------------------------------------------------------- 1 | const createUser = async (agent) => { 2 | const res = await agent.post("/authorization/register").send({ 3 | firstName: "Kunal", 4 | email: "kunal@test.com", 5 | password: "test@123", 6 | }); 7 | 8 | return res; 9 | }; 10 | 11 | module.exports = { createUser }; 12 | -------------------------------------------------------------------------------- /server/routes/user.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const { loginUser, registerUser } = require("../controllers/userController"); 4 | 5 | // login route 6 | router.post("/login", loginUser); 7 | 8 | // register route 9 | router.post("/register", registerUser); 10 | 11 | module.exports = router; -------------------------------------------------------------------------------- /client/src/components/general/ScrollToTop.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { useLocation } from "react-router-dom"; 3 | 4 | const ScrollToTop = () => { 5 | const { pathname } = useLocation(); 6 | 7 | useEffect(() => { 8 | window.scrollTo(0,0) 9 | },[pathname]) 10 | 11 | return null; 12 | } 13 | export default ScrollToTop -------------------------------------------------------------------------------- /client/src/loaders/screens/ListingScreen.js: -------------------------------------------------------------------------------- 1 | import Cards from "../partials/Cards" 2 | import Footer from "../partials/Footer" 3 | 4 | const ListingScreen = () => { 5 | return ( 6 |
7 | 8 | {/* Cards */} 9 | 10 | 11 | {/* Footer */} 12 |
15 | ) 16 | } 17 | export default ListingScreen -------------------------------------------------------------------------------- /client/cypress.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-undef 2 | const { defineConfig } = require("cypress"); 3 | 4 | // eslint-disable-next-line no-undef 5 | module.exports = defineConfig({ 6 | e2e: { 7 | // eslint-disable-next-line no-unused-vars 8 | setupNodeEvents(on, config) { 9 | // implement node event listeners here 10 | }, 11 | baseUrl: "http://localhost:3000", 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | build 13 | 14 | # misc 15 | .DS_Store 16 | .env 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /client/src/loaders/partials/Tabs.js: -------------------------------------------------------------------------------- 1 | const Tabs = () => { 2 | return ( 3 |
4 |
5 |
6 |
7 |
8 | ); 9 | }; 10 | export default Tabs; 11 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const app = require("./app"); 2 | const mongoose = require("mongoose"); 3 | 4 | // mongoDB initialization 5 | mongoose 6 | .connect(process.env.MONGODB_URI) 7 | .then(() => { 8 | app.listen(process.env.PORT, () => { 9 | console.log( 10 | "connected to mongoDB & listening on port " + process.env.PORT 11 | ); 12 | }); 13 | }) 14 | .catch((err) => { 15 | console.log(err); 16 | }); 17 | -------------------------------------------------------------------------------- /client/cypress/helpers/checkHero.js: -------------------------------------------------------------------------------- 1 | const checkHero = () => { 2 | it("Check the content of the hero", () => { 3 | cy.getCypress("hero-image-container").should("exist"); 4 | cy.getCypress("hero-image-container").should("be.visible"); 5 | cy.getCypress("hero-image-container").find("img").should("exist"); 6 | cy.getCypress("hero-image-container").find("img").should("be.visible"); 7 | }); 8 | }; 9 | 10 | export { checkHero }; 11 | -------------------------------------------------------------------------------- /server/routes/collection.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const { allCollection, addCollection, deleteCollection } = require("../controllers/collectionController"); 4 | 5 | // Get all collection 6 | router.get("/collection", allCollection); 7 | 8 | // Add collection 9 | router.post("/collection", addCollection); 10 | 11 | // Delete collection 12 | router.delete("/collection/:id", deleteCollection); 13 | 14 | module.exports = router; -------------------------------------------------------------------------------- /client/src/loaders/screens/MainScreen.js: -------------------------------------------------------------------------------- 1 | import Carousel from "../partials/Carousel"; 2 | import Footer from "../partials/Footer"; 3 | import Hero from "../partials/Hero"; 4 | 5 | const MainScreen = () => { 6 | return ( 7 |
8 | 9 | {/* Hero Container */} 10 | 11 | 12 | {/* Carousels */} 13 | 14 | 15 | {/* Carousels */} 16 | 17 | 18 | {/* Footer */} 19 |
22 | ); 23 | }; 24 | export default MainScreen; 25 | -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | import { store } from "./redux/store"; 6 | import { Provider } from "react-redux"; 7 | import * as serviceWorkerRegistration from './serviceWorkerRegistration'; 8 | 9 | const root = ReactDOM.createRoot(document.getElementById("root")); 10 | root.render( 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | 18 | serviceWorkerRegistration.register(); 19 | -------------------------------------------------------------------------------- /client/src/loaders/screens/UserDetailsScreen.js: -------------------------------------------------------------------------------- 1 | import Cards from "../partials/Cards" 2 | import Footer from "../partials/Footer" 3 | import Tabs from "../partials/Tabs" 4 | import UserDetails from "../partials/UserDetails" 5 | 6 | const UserDetailsScreen = () => { 7 | return ( 8 |
9 | {/* User Details */} 10 | 11 | {/* Tabs */} 12 | 13 | {/* Cards */} 14 | 15 | {/* Footer */} 16 |
18 | ) 19 | } 20 | export default UserDetailsScreen -------------------------------------------------------------------------------- /client/src/loaders/partials/Hero.js: -------------------------------------------------------------------------------- 1 | const Hero = () => { 2 | return ( 3 |
4 | {/* Texts */} 5 |
6 |
7 |
8 |
9 |
10 | ); 11 | }; 12 | export default Hero; 13 | -------------------------------------------------------------------------------- /client/src/components/general/BackNavigation.js: -------------------------------------------------------------------------------- 1 | import { FaChevronLeft } from "@react-icons/all-files/fa/FaChevronLeft"; 2 | import { useNavigate } from "react-router-dom"; 3 | 4 | const BackNavigation = () => { 5 | const navigate = useNavigate(); 6 | return ( 7 |
8 | 13 | Go back 14 |
15 | ) 16 | } 17 | export default BackNavigation -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | my_movies_client: 3 | build: 4 | context: ./client 5 | dockerfile: Dockerfile.client 6 | env_file: 7 | - path: ./client/.env 8 | ports: 9 | - "3000:3000" 10 | depends_on: 11 | - db 12 | my_movies_server: 13 | build: 14 | context: ./server 15 | dockerfile: Dockerfile.server 16 | env_file: 17 | - path: ./server/.env 18 | ports: 19 | - "8000:8000" 20 | depends_on: 21 | - db 22 | db: 23 | volumes: 24 | - my_movies:/data/db 25 | image: mongo:latest 26 | ports: 27 | - "27017:27017" 28 | volumes: 29 | my_movies: -------------------------------------------------------------------------------- /server/models/collection.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const Schema = mongoose.Schema; 4 | 5 | const userCollection = new Schema({ 6 | mediaId: { 7 | type: Number, 8 | required: true 9 | }, 10 | title: { 11 | type: String, 12 | required: true, 13 | }, 14 | mediaType: { 15 | type: String, 16 | required: true 17 | }, 18 | saveType: { 19 | type: String, 20 | required: true 21 | }, 22 | userId: { 23 | type: String, 24 | required: true 25 | } 26 | }, {timestamps: true}); 27 | 28 | module.exports = mongoose.model("Collection", userCollection); -------------------------------------------------------------------------------- /client/src/redux/store.js: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit' 2 | import layoutReducer from "./layoutSlice"; 3 | import genreReducer from "./genreSlice"; 4 | import userReducer from "./userSlice"; 5 | import { tmdbApi } from './tmdbApi'; 6 | import { userApi } from './userApi'; 7 | 8 | export const store = configureStore({ 9 | reducer: { 10 | layout: layoutReducer, 11 | genre: genreReducer, 12 | user: userReducer, 13 | [tmdbApi.reducerPath]: tmdbApi.reducer, 14 | [userApi.reducerPath]: userApi.reducer 15 | }, 16 | 17 | middleware: (getDefaultMiddleware) => 18 | getDefaultMiddleware().concat(tmdbApi.middleware, userApi.middleware) 19 | }) -------------------------------------------------------------------------------- /client/src/hooks/useUniqueData.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | const useUniqueData = (data) => { 4 | const [uniqueItems, setUniqueItems] = useState([]); 5 | 6 | useEffect(() => { 7 | if (data) { 8 | const uniqueIds = []; 9 | setUniqueItems(data.filter(element => { 10 | const isDuplicate = uniqueIds.includes(element.id); 11 | 12 | if (!isDuplicate) { 13 | uniqueIds.push(element.id); 14 | 15 | return true; 16 | } 17 | 18 | return false; 19 | })) 20 | } 21 | }, [data]) 22 | 23 | 24 | return { uniqueItems } 25 | } 26 | export default useUniqueData -------------------------------------------------------------------------------- /client/src/loaders/screens/DetailsScreen.js: -------------------------------------------------------------------------------- 1 | import Carousel from "../partials/Carousel"; 2 | import Details from "../partials/Details"; 3 | import Footer from "../partials/Footer"; 4 | import Hero from "../partials/Hero"; 5 | import Tabs from "../partials/Tabs"; 6 | 7 | const DetailsScreen = () => { 8 | return ( 9 |
10 | 11 | {/* Hero Container */} 12 | 13 | 14 | {/* Tabs */} 15 | 16 | 17 | {/* Details */} 18 |
19 | 20 | {/* Carousels */} 21 | 22 | 23 | {/* Carousels */} 24 | 25 | 26 | {/* Footer */} 27 |
30 | ); 31 | }; 32 | export default DetailsScreen; 33 | -------------------------------------------------------------------------------- /client/cypress/support/e2e.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import "./commands"; 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /client/src/components/general/Category.js: -------------------------------------------------------------------------------- 1 | import CardListing from "./CardListing"; 2 | 3 | const Category = ({ data, isFetching, heading, totalPages, mediaType }) => { 4 | return ( 5 | // Wrapper 6 |
7 | {/* Heading */} 8 |
9 |

13 | {heading} 14 |

15 |
16 | {/* Container */} 17 | 23 |
24 | ); 25 | }; 26 | export default Category; 27 | -------------------------------------------------------------------------------- /client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "myMovies", 3 | "name": "Your own personal movies, shows, and artist collection store.", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "maskable-icon.png", 17 | "type": "image/png", 18 | "sizes": "192x192", 19 | "purpose": "maskable" 20 | }, 21 | { 22 | "src": "logo512.png", 23 | "type": "image/png", 24 | "sizes": "512x512" 25 | } 26 | ], 27 | "start_url": ".", 28 | "display": "standalone", 29 | "theme_color": "#000000", 30 | "background_color": "#111111" 31 | } 32 | -------------------------------------------------------------------------------- /server/middleware/userAuthorization.js: -------------------------------------------------------------------------------- 1 | const jwt = require("jsonwebtoken"); 2 | const User = require("../models/user"); 3 | 4 | const userAuthorization = async (req, res, next) => { 5 | // verify authorization 6 | const { authorization } = req.headers; 7 | 8 | if (!authorization) { 9 | return res.status(401).json({ error: "Authorization token required." }); 10 | } 11 | 12 | // token 13 | const token = authorization.split(" ")[1]; 14 | 15 | try { 16 | // get id from token 17 | const { _id } = await jwt.verify(token, process.env.JWT_SECRET_KEY); 18 | // pass user with id after authorization 19 | req.user = await User.findOne({ _id }).select("_id"); 20 | next(); 21 | } catch (error) { 22 | console.error(error); 23 | res.status(401).json({ error: "Request is not authorized." }); 24 | } 25 | }; 26 | 27 | module.exports = userAuthorization; 28 | -------------------------------------------------------------------------------- /client/cypress/e2e/home/home.cy.js: -------------------------------------------------------------------------------- 1 | import { checkCarousel } from "../../helpers/checkCarousel"; 2 | import { checkFooter } from "../../helpers/checkFooter"; 3 | import { checkHero } from "../../helpers/checkHero"; 4 | import { checkHeroTypography } from "../../helpers/checkHeroTypography"; 5 | import { checkNavigation } from "../../helpers/checkNavigation"; 6 | 7 | describe("Check content of the homepage", () => { 8 | // Visit the landing page 9 | beforeEach(() => { 10 | cy.visit("/"); 11 | }); 12 | 13 | // Check the content of the hero 14 | checkHero(); 15 | 16 | // Check the typography of the hero 17 | checkHeroTypography(); 18 | 19 | // Check carousel of movies & shows 20 | checkCarousel("Trending Movies", "Trending Shows", "Explore All"); 21 | 22 | // Check the footer 23 | checkFooter(); 24 | 25 | // Check the navigation 26 | checkNavigation(); 27 | }); 28 | -------------------------------------------------------------------------------- /client/cypress/helpers/checkNavigation.js: -------------------------------------------------------------------------------- 1 | const checkNavigation = () => { 2 | it("Check the navigation", () => { 3 | cy.getCypress("navigation-container").should("exist"); 4 | cy.getCypress("navigation-container").should("be.visible"); 5 | cy.getCypress("navigation-home").should("exist"); 6 | cy.getCypress("navigation-home").should("be.visible"); 7 | cy.getCypress("navigation-search").should("exist"); 8 | cy.getCypress("navigation-search").should("be.visible"); 9 | cy.getCypress("navigation-movies").should("exist"); 10 | cy.getCypress("navigation-movies").should("be.visible"); 11 | cy.getCypress("navigation-shows").should("exist"); 12 | cy.getCypress("navigation-shows").should("be.visible"); 13 | cy.getCypress("navigation-user").should("exist"); 14 | cy.getCypress("navigation-user").should("be.visible"); 15 | }); 16 | }; 17 | 18 | export { checkNavigation }; 19 | -------------------------------------------------------------------------------- /client/src/pages/Account.js: -------------------------------------------------------------------------------- 1 | import Footer from "../components/general/Footer"; 2 | import UserForm from "../components/user/UserForm"; 3 | import UserProfile from "../components/user/UserProfile"; 4 | import { useDispatch, useSelector } from "react-redux"; 5 | import { checkUser } from "../redux/userSlice"; 6 | import { useEffect } from "react"; 7 | import DetailsScreen from "../loaders/screens/DetailsScreen"; 8 | 9 | const Account = () => { 10 | const { user, userStatus } = useSelector(state => state.user); 11 | const dispatch = useDispatch(); 12 | 13 | useEffect(() => { 14 | dispatch(checkUser()); 15 | },[dispatch]); 16 | 17 | return ( 18 |
19 | {userStatus && } 20 | {!user && !userStatus && } 21 | {user && !userStatus && } 22 |
24 | ) 25 | } 26 | export default Account -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | const cors = require("cors"); 3 | const express = require("express"); 4 | const app = express(); 5 | const tmdbRoutes = require("./routes/tmdb"); 6 | const userRoutes = require("./routes/user"); 7 | const collectionRoutes = require("./routes/collection"); 8 | const userAuthorization = require("./middleware/userAuthorization"); 9 | const path = require("path"); 10 | 11 | // only when ready to deploy 12 | app.use(express.static("../client/build")); 13 | 14 | // middleware 15 | app.use(cors()); 16 | app.use(express.json()); 17 | 18 | // tmdb 19 | app.use("/api", tmdbRoutes); 20 | 21 | // User Authorization 22 | app.use("/authorization", userRoutes); 23 | 24 | // authorization middleware 25 | app.use(userAuthorization); 26 | app.use("/user", collectionRoutes); 27 | 28 | // only when ready to deploy 29 | app.get("*", (req, res) => 30 | res.sendFile(path.resolve(__dirname, "../client", "build", "index.html")) 31 | ); 32 | 33 | module.exports = app; 34 | -------------------------------------------------------------------------------- /client/cypress/e2e/shows/shows.cy.js: -------------------------------------------------------------------------------- 1 | import { checkCarousel } from "../../helpers/checkCarousel"; 2 | import { checkFooter } from "../../helpers/checkFooter"; 3 | import { checkHero } from "../../helpers/checkHero"; 4 | import { checkHeroTypography } from "../../helpers/checkHeroTypography"; 5 | import { checkNavigation } from "../../helpers/checkNavigation"; 6 | 7 | describe("Check content of the shows page", () => { 8 | // Visit the shows page 9 | beforeEach(() => { 10 | cy.visit("/shows"); 11 | }); 12 | 13 | // Check the content of the hero 14 | checkHero(); 15 | 16 | // Check the typography of the hero 17 | checkHeroTypography(); 18 | 19 | // Check carousel of shows 20 | checkCarousel("Shows Airing Today", "Popular Shows", "Explore All"); 21 | 22 | // Check carousel of shows 23 | checkCarousel("Top-Rated Shows", "Shows On The Air", "Explore All"); 24 | 25 | // Check the footer 26 | checkFooter(); 27 | 28 | // Check the navigation 29 | checkNavigation(); 30 | }); 31 | -------------------------------------------------------------------------------- /client/src/redux/userSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit' 2 | 3 | const initialState = { 4 | user: null, 5 | userStatus: false, 6 | userInfo: null, 7 | } 8 | 9 | export const userSlice = createSlice({ 10 | name: "user", 11 | initialState, 12 | reducers: { 13 | setUser: (state, action) => { 14 | state.user = action.payload; 15 | localStorage.setItem("user", JSON.stringify(action.payload)); 16 | }, 17 | removeUser: (state) => { 18 | state.user = null; 19 | localStorage.removeItem("user"); 20 | }, 21 | checkUser: (state) => { 22 | state.userStatus = true; 23 | const user = JSON.parse(localStorage.getItem("user")); 24 | if (user) state.user = user; 25 | state.userStatus = false; 26 | }, 27 | setUserInfo: (state, action) => { 28 | state.userInfo = action.payload; 29 | } 30 | }, 31 | }) 32 | 33 | export const { setUser, removeUser, checkUser, setUserInfo } = userSlice.actions 34 | export default userSlice.reducer -------------------------------------------------------------------------------- /client/cypress/e2e/movies/movies.cy.js: -------------------------------------------------------------------------------- 1 | import { checkCarousel } from "../../helpers/checkCarousel"; 2 | import { checkFooter } from "../../helpers/checkFooter"; 3 | import { checkHero } from "../../helpers/checkHero"; 4 | import { checkHeroTypography } from "../../helpers/checkHeroTypography"; 5 | import { checkNavigation } from "../../helpers/checkNavigation"; 6 | 7 | describe("Check content of the movies page", () => { 8 | // Visit the movies page 9 | beforeEach(() => { 10 | cy.visit("/movies"); 11 | }); 12 | 13 | // Check the content of the hero 14 | checkHero(); 15 | 16 | // Check the typography of the hero 17 | checkHeroTypography(); 18 | 19 | // Check carousel of movies & shows 20 | checkCarousel("Movies Now Playing", "Popular Movies", "Explore All"); 21 | 22 | // Check carousel of movies & shows 23 | checkCarousel("Top-Rated Movies", "Upcoming Movies", "Explore All"); 24 | 25 | // Check the footer 26 | checkFooter(); 27 | 28 | // Check the navigation 29 | checkNavigation(); 30 | }); 31 | -------------------------------------------------------------------------------- /client/src/pages/details/PersonDetails.js: -------------------------------------------------------------------------------- 1 | import { useParams } from "react-router-dom"; 2 | import ArtistDetails from "../../components/artist/ArtistDetails"; 3 | import BackNavigation from "../../components/general/BackNavigation"; 4 | import Footer from "../../components/general/Footer"; 5 | import { useGetPersonDetailsPageQuery } from "../../redux/tmdbApi"; 6 | import PersonDetailsScreen from "../../loaders/screens/UserDetailsScreen"; 7 | 8 | const PersonDetails = () => { 9 | const { id } = useParams(); 10 | const { data, isLoading } = useGetPersonDetailsPageQuery(id); 11 | return ( 12 |
13 | {!isLoading && data ? ( 14 | <> 15 | {/* Back Navigation */} 16 | 17 | {/* Artist Details */} 18 | 19 | {/* Footer */} 20 |
26 | ); 27 | }; 28 | export default PersonDetails; 29 | -------------------------------------------------------------------------------- /.github/workflows/deployment-client.yml: -------------------------------------------------------------------------------- 1 | name: Perform Test & Deploy to Client 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths: 7 | - client/** 8 | - .github/workflows/deployment-client.yml 9 | jobs: 10 | test-client: 11 | runs-on: ubuntu-latest 12 | env: 13 | REACT_APP_BACKEND_URL: ${{ secrets.REACT_APP_BACKEND_URL }} 14 | steps: 15 | - name: Get Code 16 | uses: actions/checkout@v4 17 | - name: Setup Node.js 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: 20 21 | - name: Cypress Run 22 | uses: cypress-io/github-action@v6 23 | with: 24 | working-directory: client 25 | start: npm start 26 | deploy-client: 27 | runs-on: ubuntu-latest 28 | needs: test-client 29 | steps: 30 | - name: Get Code 31 | uses: actions/checkout@v4 32 | - name: Deploy 33 | env: 34 | deploy_url: ${{ secrets.RENDER_CLIENT_DEPLOY_HOOK_URL }} 35 | run: | 36 | curl "$deploy_url" 37 | -------------------------------------------------------------------------------- /client/cypress/helpers/checkHeroTypography.js: -------------------------------------------------------------------------------- 1 | const checkHeroTypography = () => { 2 | it("Check the typography of the hero", () => { 3 | cy.getCypress("hero-title").should("exist"); 4 | cy.getCypress("hero-title").should("be.visible"); 5 | cy.getCypress("hero-description").should("exist"); 6 | cy.getCypress("hero-description").should("be.visible"); 7 | cy.getCypress("hero-rating").should("exist"); 8 | cy.getCypress("hero-rating").should("be.visible"); 9 | cy.getCypress("hero-rating-container").should("exist"); 10 | cy.getCypress("hero-rating-container").should("be.visible"); 11 | cy.getCypress("hero-rating-reviews-count").should("exist"); 12 | cy.getCypress("hero-rating-reviews-count").should("be.visible"); 13 | cy.getCypress("hero-rating-reviews-count").should( 14 | "contain.text", 15 | "Reviews" 16 | ); 17 | cy.getCypress("hero-season-certification-container").should("exist"); 18 | cy.getCypress("hero-season-certification-container").should("be.visible"); 19 | }); 20 | }; 21 | 22 | export { checkHeroTypography }; 23 | -------------------------------------------------------------------------------- /client/cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add('login', (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 26 | 27 | Cypress.Commands.add("getCypress", (attribute) => { 28 | return cy.get(`[data-cy=${attribute}]`); 29 | }); 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Kunal Ukey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /client/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | const defaultTheme = require("tailwindcss/defaultTheme"); 3 | 4 | module.exports = { 5 | content: ["./src/**/*.{js,jsx,ts,tsx}"], 6 | theme: { 7 | screens: { 8 | xs: "300px", 9 | ...defaultTheme.screens, 10 | }, 11 | extend: { 12 | boxShadow: { 13 | "hero-top-super-small": "0px 15px 17.5px 37.5px rgba(0,0,0,1)", 14 | "hero-top-small": "0px 30px 35px 75px rgba(0,0,0,1)", 15 | "hero-top": "0px -10px 100px 100px rgba(0,0,0,1)", 16 | "hero-right": "-990vw 0px 8vw 1000vw rgba(0,0,0,1)", 17 | }, 18 | width: { 19 | "card-x-small": "calc(33.33% - 7.33326px)", 20 | "card-small": "calc(8.5% - 18px)", 21 | "card-large": "calc(20% - 14.4px)", 22 | }, 23 | colors: { 24 | nav: "#000000", 25 | background: "#111111", 26 | card: "#222", 27 | tabs: "#333", 28 | "tab-active": "#444", 29 | }, 30 | }, 31 | }, 32 | plugins: [require("daisyui")], 33 | 34 | // daisyUI config (optional) 35 | daisyui: { 36 | themes: ["dark"] 37 | }, 38 | }; 39 | -------------------------------------------------------------------------------- /client/src/loaders/partials/Carousel.js: -------------------------------------------------------------------------------- 1 | const Carousel = () => { 2 | return ( 3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | ); 16 | }; 17 | export default Carousel; 18 | -------------------------------------------------------------------------------- /.github/workflows/deployment-server.yml: -------------------------------------------------------------------------------- 1 | name: Perform Test & Deploy to Server 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths: 7 | - server/** 8 | - .github/workflows/deployment-server.yml 9 | jobs: 10 | test-server: 11 | runs-on: ubuntu-latest 12 | env: 13 | JWT_SECRET_KEY: ${{ secrets.JWT_SECRET_KEY }} 14 | BASE_URL: ${{ secrets.BASE_URL }} 15 | API_KEY: ${{ secrets.API_KEY }} 16 | PORT: ${{ secrets.PORT }} 17 | steps: 18 | - name: Get Code 19 | uses: actions/checkout@v4 20 | - name: Setup Node.js 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: 20 24 | - name: Install Dependencies 25 | working-directory: server 26 | run: npm install 27 | - name: Run Tests 28 | working-directory: server 29 | run: npm run test 30 | deploy-server: 31 | runs-on: ubuntu-latest 32 | needs: test-server 33 | steps: 34 | - name: Get Code 35 | uses: actions/checkout@v4 36 | - name: Deploy 37 | env: 38 | deploy_url: ${{ secrets.RENDER_SERVER_DEPLOY_HOOK_URL }} 39 | run: | 40 | curl "$deploy_url" 41 | -------------------------------------------------------------------------------- /server/controllers/userController.js: -------------------------------------------------------------------------------- 1 | const User = require("../models/user"); 2 | const jwt = require("jsonwebtoken"); 3 | 4 | // Create JWT token 5 | const createToken = (_id) => { 6 | return jwt.sign({_id}, process.env.JWT_SECRET_KEY, {expiresIn: "30 days"}); 7 | } 8 | 9 | // login 10 | const loginUser = async (req, res) => { 11 | const { email, password } = req.body; 12 | 13 | try { 14 | const user = await User.login(email, password); 15 | const token = await createToken(user._id); 16 | res.status(200).json({userId: user._id, firstName: user.firstName, token}); 17 | } catch (error) { 18 | res.status(400).json({error: error.message}); 19 | } 20 | }; 21 | 22 | // register 23 | const registerUser = async (req, res) => { 24 | const { firstName, email, password } = req.body; 25 | 26 | try { 27 | const user = await User.register(firstName, email, password); 28 | const token = await createToken(user._id); 29 | res.status(200).json({userId: user._id, firstName: user.firstName, token}); 30 | } catch (error) { 31 | res.status(400).json({error: error.message}); 32 | } 33 | }; 34 | 35 | module.exports = { loginUser, registerUser }; -------------------------------------------------------------------------------- /.github/workflows/testing.yml: -------------------------------------------------------------------------------- 1 | name: Perform Testing 2 | on: 3 | push: 4 | branches-ignore: 5 | - main 6 | jobs: 7 | test-client: 8 | runs-on: ubuntu-latest 9 | env: 10 | REACT_APP_BACKEND_URL: ${{ secrets.REACT_APP_BACKEND_URL }} 11 | steps: 12 | - name: Get Code 13 | uses: actions/checkout@v4 14 | - name: Setup Node.js 15 | uses: actions/setup-node@v4 16 | with: 17 | node-version: 20 18 | - name: Cypress Run 19 | uses: cypress-io/github-action@v6 20 | with: 21 | working-directory: client 22 | start: npm start 23 | test-server: 24 | runs-on: ubuntu-latest 25 | env: 26 | JWT_SECRET_KEY: ${{ secrets.JWT_SECRET_KEY }} 27 | BASE_URL: ${{ secrets.BASE_URL }} 28 | API_KEY: ${{ secrets.API_KEY }} 29 | PORT: ${{ secrets.PORT }} 30 | steps: 31 | - name: Get Code 32 | uses: actions/checkout@v4 33 | - name: Setup Node.js 34 | uses: actions/setup-node@v4 35 | with: 36 | node-version: 20 37 | - name: Install Dependencies 38 | working-directory: server 39 | run: npm install 40 | - name: Run Tests 41 | working-directory: server 42 | run: npm run test 43 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node server", 8 | "dev": "nodemon server", 9 | "lint:staged": "lint-staged", 10 | "test": "jest" 11 | }, 12 | "lint-staged": { 13 | "**/*": "prettier --write --ignore-unknown", 14 | "*.js": "eslint --cache --fix --max-warnings=0", 15 | "**/*.js": "prettier --write" 16 | }, 17 | "jest": { 18 | "testEnvironment": "node" 19 | }, 20 | "keywords": [], 21 | "author": "", 22 | "license": "ISC", 23 | "dependencies": { 24 | "axios": "^1.1.3", 25 | "bcrypt": "^5.1.0", 26 | "cors": "^2.8.5", 27 | "dotenv": "^16.0.3", 28 | "express": "^4.18.2", 29 | "jsonwebtoken": "^8.5.1", 30 | "mongoose": "^6.6.7", 31 | "validator": "^13.7.0" 32 | }, 33 | "devDependencies": { 34 | "@eslint/eslintrc": "^3.0.2", 35 | "@eslint/js": "^9.0.0", 36 | "eslint": "^9.0.0", 37 | "eslint-config-prettier": "^9.1.0", 38 | "eslint-plugin-jest": "^28.2.0", 39 | "globals": "^15.0.0", 40 | "jest": "^29.7.0", 41 | "lint-staged": "^15.2.2", 42 | "mongodb-memory-server": "^9.1.8", 43 | "prettier": "3.2.5", 44 | "supertest": "^6.3.4" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /server/__test__/db.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const { MongoMemoryServer } = require("mongodb-memory-server"); 3 | 4 | let mongoServer; 5 | 6 | // For mongodb-memory-server's old version (< 7) use this instead: 7 | // const mongoServer = new MongoMemoryServer(); 8 | 9 | const opts = { 10 | useNewUrlParser: true, 11 | useUnifiedTopology: true, 12 | }; 13 | 14 | // Provide connection to a new in-memory database server. 15 | const connect = async () => { 16 | // NOTE: before establishing a new connection close previous 17 | await mongoose.disconnect(); 18 | 19 | mongoServer = await MongoMemoryServer.create(); 20 | 21 | const mongoUri = await mongoServer.getUri(); 22 | await mongoose.connect(mongoUri, opts, (err) => { 23 | if (err) { 24 | console.error(err); 25 | } 26 | }); 27 | }; 28 | 29 | // Remove and close the database and server. 30 | const close = async () => { 31 | await mongoose.disconnect(); 32 | await mongoServer.stop(); 33 | }; 34 | 35 | // Remove all data from collections 36 | const clear = async () => { 37 | const collections = mongoose.connection.collections; 38 | 39 | for (const key in collections) { 40 | await collections[key].deleteMany(); 41 | } 42 | }; 43 | 44 | module.exports = { 45 | connect, 46 | close, 47 | clear, 48 | }; 49 | -------------------------------------------------------------------------------- /server/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | import pluginJs from "@eslint/js"; 3 | import jest from "eslint-plugin-jest"; 4 | 5 | // mimic CommonJS variables -- not needed if using CommonJS 6 | import { FlatCompat } from "@eslint/eslintrc"; 7 | import path from "path"; 8 | import { fileURLToPath } from "url"; 9 | import eslintConfigPrettier from "eslint-config-prettier"; 10 | 11 | // mimic CommonJS variables -- not needed if using CommonJS 12 | const __filename = fileURLToPath(import.meta.url); 13 | const __dirname = path.dirname(__filename); 14 | 15 | const compat = new FlatCompat({ 16 | baseDirectory: __dirname, 17 | }); 18 | 19 | export default [ 20 | { files: ["**/*.js"], languageOptions: { sourceType: "commonjs" } }, 21 | { languageOptions: { globals: globals.node } }, 22 | pluginJs.configs.recommended, 23 | ...compat.extends("prettier"), 24 | { 25 | rules: { 26 | "no-unused-vars": "error", 27 | "no-undef": "error", 28 | }, 29 | }, 30 | { 31 | ignores: [ 32 | "node_modules", 33 | ".env", 34 | ".env_sample", 35 | ".prettierignore", 36 | ".prettierrc", 37 | ], 38 | }, 39 | { 40 | files: ["**/*.test.js", "**/*.spec.js"], 41 | ...jest.configs["flat/recommended"], 42 | }, 43 | eslintConfigPrettier, 44 | ]; 45 | -------------------------------------------------------------------------------- /client/src/pages/Home.js: -------------------------------------------------------------------------------- 1 | import Hero from "../components/general/Hero"; 2 | import MoviesCarousel from "../components/carousel/Carousel"; 3 | import ShowsCarousel from "../components/carousel/Carousel"; 4 | import Footer from "../components/general/Footer"; 5 | import { useGetSingleTrendingQuery, useGetTrendingQuery } from "../redux/tmdbApi"; 6 | import MainScreen from "../loaders/screens/MainScreen"; 7 | 8 | const Home = () => { 9 | const { data, isLoading: heroLoading, error } = useGetSingleTrendingQuery("all"); 10 | const { data: list, isLoading: moviesLoading } = useGetTrendingQuery("movie"); 11 | const { data: shows, isLoading: showsLoading } = useGetTrendingQuery("tv"); 12 | 13 | return ( 14 |
15 | {!heroLoading && !moviesLoading && !showsLoading && data && list && shows ? 16 | <> 17 | 18 | 23 | 28 |
34 | ); 35 | }; 36 | export default Home; 37 | -------------------------------------------------------------------------------- /client/src/redux/layoutSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const initialState = { 4 | isSearch: false, 5 | backdrop: "https://image.tmdb.org/t/p/original/", 6 | poster: "https://image.tmdb.org/t/p/w370_and_h556_bestv2/", 7 | page: 1, 8 | searchQuery: "", 9 | cardAction: false, 10 | castCardAction: false, 11 | }; 12 | 13 | export const layoutSlice = createSlice({ 14 | name: "layout", 15 | initialState, 16 | reducers: { 17 | setSearch: (state, action) => { 18 | state.isSearch = action.payload; 19 | }, 20 | setPage: (state) => { 21 | state.page += 1; 22 | }, 23 | resetPage: (state) => { 24 | state.page = 1; 25 | }, 26 | setSearchQuery: (state, action) => { 27 | state.searchQuery = action.payload; 28 | }, 29 | resetSearchQuery: (state) => { 30 | state.searchQuery = ""; 31 | }, 32 | setCardAction: (state, action) => { 33 | state.cardAction = action.payload; 34 | }, 35 | setCastCardAction: (state, action) => { 36 | state.castCardAction = action.payload; 37 | } 38 | }, 39 | }); 40 | 41 | export const { 42 | setSearch, 43 | setPage, 44 | resetPage, 45 | setSearchQuery, 46 | resetSearchQuery, 47 | setCardAction, 48 | setCastCardAction 49 | } = layoutSlice.actions; 50 | export default layoutSlice.reducer; 51 | -------------------------------------------------------------------------------- /client/src/loaders/partials/Details.js: -------------------------------------------------------------------------------- 1 | const Details = () => { 2 | return ( 3 |
4 | {/* Image */} 5 |
6 | {/* Texts */} 7 |
8 |
9 |
10 |
11 |
12 | 13 |
14 |
15 |
16 |
17 |
18 | 19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | ); 27 | }; 28 | export default Details; 29 | -------------------------------------------------------------------------------- /client/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | import pluginJs from "@eslint/js"; 3 | import pluginReactConfig from "eslint-plugin-react/configs/recommended.js"; 4 | import eslintConfigPrettier from "eslint-config-prettier"; 5 | 6 | import { FlatCompat } from "@eslint/eslintrc"; 7 | import path from "path"; 8 | import { fileURLToPath } from "url"; 9 | 10 | // mimic CommonJS variables -- not needed if using CommonJS 11 | const __filename = fileURLToPath(import.meta.url); 12 | const __dirname = path.dirname(__filename); 13 | 14 | const compat = new FlatCompat({ 15 | baseDirectory: __dirname, 16 | }); 17 | 18 | export default [ 19 | { 20 | settings: { 21 | react: { 22 | version: "detect", 23 | }, 24 | }, 25 | }, 26 | { languageOptions: { globals: globals.browser } }, 27 | pluginJs.configs.recommended, 28 | pluginReactConfig, 29 | ...compat.extends("plugin:cypress/recommended"), 30 | { 31 | rules: { 32 | "no-unused-vars": "error", 33 | "no-undef": "error", 34 | "react/jsx-uses-react": "off", 35 | "react/react-in-jsx-scope": "off", 36 | "react/prop-types": "off", 37 | }, 38 | }, 39 | { 40 | ignores: [ 41 | "node_modules", 42 | "cypress", 43 | "postcss.config.js", 44 | "src/service-worker.js", 45 | "src/serviceWorkerRegistration.js", 46 | "tailwind.config.js", 47 | ], 48 | }, 49 | eslintConfigPrettier, 50 | ]; 51 | -------------------------------------------------------------------------------- /client/cypress/helpers/checkCarousel.js: -------------------------------------------------------------------------------- 1 | const checkCarousel = (heading1, heading2, exploreButton) => { 2 | it("Check carousel of movies & shows", () => { 3 | cy.getCypress("carousel-heading-container").should("exist"); 4 | cy.getCypress("carousel-heading-container").should("be.visible"); 5 | cy.getCypress("carousel-heading-text").should("exist"); 6 | cy.getCypress("carousel-heading-text").should("be.visible"); 7 | if (heading1) { 8 | cy.getCypress("carousel-heading-text").should("contain.text", heading1); 9 | } 10 | if (heading2) { 11 | cy.getCypress("carousel-heading-text").should("contain.text", heading2); 12 | } 13 | if (exploreButton) { 14 | cy.getCypress("carousel-heading-explore-button").should("exist"); 15 | cy.getCypress("carousel-heading-explore-button").should("be.visible"); 16 | cy.getCypress("carousel-heading-explore-button").should( 17 | "contain.text", 18 | exploreButton 19 | ); 20 | } 21 | cy.getCypress("carousel-items-container").should("exist"); 22 | cy.getCypress("carousel-items-container").should("be.visible"); 23 | cy.getCypress("carousel-items-container").find("img").should("exist"); 24 | cy.getCypress("carousel-items-container").find("img").should("be.visible"); 25 | cy.getCypress("carousel-item-link").should("exist"); 26 | cy.getCypress("carousel-item-link").should("be.visible"); 27 | }); 28 | }; 29 | 30 | export { checkCarousel }; 31 | -------------------------------------------------------------------------------- /client/src/pages/details/MovieDetails.js: -------------------------------------------------------------------------------- 1 | import BackNavigation from "../../components/general/BackNavigation"; 2 | import Hero from "../../components/general/Hero"; 3 | import DetailsTab from "../../components/details/DetailsTab"; 4 | import Cast from "../../components/carousel/CastCarousel"; 5 | import MoreLikeThis from "../../components/carousel/Carousel"; 6 | import Footer from "../../components/general/Footer"; 7 | import { useParams } from "react-router-dom"; 8 | import { useGetMovieDetailsPageQuery } from "../../redux/tmdbApi"; 9 | import DetailsScreen from "../../loaders/screens/DetailsScreen"; 10 | 11 | const MovieDetails = () => { 12 | const { id } = useParams(); 13 | const { data, isLoading, error } = useGetMovieDetailsPageQuery(id); 14 | return ( 15 |
16 | {!isLoading && data ? 17 | <> 18 | {/* Back Navigation */} 19 | 20 | {/* Hero */} 21 | 22 | {/* Details Tab */} 23 | 24 | {/* Cast */} 25 | 26 | {/* More Like This */} 27 | 28 | {/* Footer */} 29 |
35 | ) 36 | } 37 | export default MovieDetails -------------------------------------------------------------------------------- /client/src/components/user/UserPerson.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import UserListing from "./UserListing"; 3 | import { FaRegTired } from "@react-icons/all-files/fa/FaRegTired"; 4 | 5 | const UserPerson = ({data}) => { 6 | const [saveType, setSaveType] = useState(""); 7 | 8 | const handleSaveType = (e) => { 9 | setSaveType(e.target.value); 10 | } 11 | 12 | return ( 13 |
14 | 15 | {data && data.length ? 16 | <> 17 | {/* Filter */} 18 |
19 | 28 |
29 | 30 | {/* User Listing */} 31 | 32 | 33 | : 34 | // No result to show 35 |
36 | 37 |

Sorry, No Result to Show!

38 |
39 | } 40 |
41 | ) 42 | } 43 | export default UserPerson -------------------------------------------------------------------------------- /client/src/components/user/UserShows.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import UserListing from "./UserListing"; 3 | import { FaRegTired } from "@react-icons/all-files/fa/FaRegTired"; 4 | 5 | const UserShows = ({data}) => { 6 | const [saveType, setSaveType] = useState(""); 7 | 8 | const handleSaveType = (e) => { 9 | setSaveType(e.target.value); 10 | } 11 | 12 | return ( 13 |
14 | 15 | {data && data.length ? 16 | <> 17 | {/* Filter */} 18 |
19 | 28 |
29 | 30 | {/* User Listing */} 31 | 32 | 33 | : 34 | // No result to show 35 |
36 | 37 |

Sorry, No Result to Show!

38 |
39 | } 40 |
41 | ) 42 | } 43 | export default UserShows -------------------------------------------------------------------------------- /client/src/components/user/UserMovies.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react" 2 | import UserListing from "./UserListing"; 3 | import { FaRegTired } from "@react-icons/all-files/fa/FaRegTired"; 4 | 5 | const UserMovies = ({data}) => { 6 | const [saveType, setSaveType] = useState(""); 7 | 8 | const handleSaveType = (e) => { 9 | setSaveType(e.target.value); 10 | } 11 | 12 | return ( 13 |
14 | 15 | {data && data.length ? 16 | <> 17 | {/* Filter */} 18 |
19 | 28 |
29 | 30 | {/* User Listing */} 31 | 32 | 33 | : 34 | // No result to show 35 |
36 | 37 |

Sorry, No Result to Show!

38 |
39 | } 40 |
41 | ) 42 | } 43 | export default UserMovies -------------------------------------------------------------------------------- /client/src/pages/details/ShowDetails.js: -------------------------------------------------------------------------------- 1 | import BackNavigation from "../../components/general/BackNavigation"; 2 | import Hero from "../../components/general/Hero"; 3 | import DetailsTab from "../../components/details/DetailsTab"; 4 | import Cast from "../../components/carousel/CastCarousel"; 5 | import MoreLikeThis from "../../components/carousel/Carousel"; 6 | import Footer from "../../components/general/Footer"; 7 | import { useParams } from "react-router-dom"; 8 | import { useGetShowDetailsPageQuery } from "../../redux/tmdbApi"; 9 | import DetailsScreen from "../../loaders/screens/DetailsScreen"; 10 | 11 | const ShowDetails = () => { 12 | const { id } = useParams(); 13 | const { data, isLoading, error } = useGetShowDetailsPageQuery(id); 14 | 15 | return ( 16 |
17 | {!isLoading && data 18 | ? 19 | <> 20 | {/* Back Navigation */} 21 | 22 | {/* Hero */} 23 | 24 | {/* Details Tab */} 25 | 26 | {/* Cast */} 27 | 28 | {/* More Like This */} 29 | 30 | {/* Footer */} 31 |
37 | ) 38 | } 39 | export default ShowDetails -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mymovies", 3 | "version": "1.0.0", 4 | "description": "Your own personal movies, shows, and artist collection store.", 5 | "main": "index.js", 6 | "scripts": { 7 | "build-client": "cd client && npm run build", 8 | "install-client": "cd client && npm install", 9 | "install-server": "cd server && npm install", 10 | "setup-production": "npm run install-client && npm run build-client && npm run install-server", 11 | "start-server": "cd server && node server.js", 12 | "start-client": "cd client && npm run start", 13 | "start-production": "npm run start-server", 14 | "prepare": "husky install", 15 | "lint": "npm-run-all lint:backend lint:frontend", 16 | "lint:backend": "cd server && npm run lint:staged", 17 | "lint:frontend": "cd client && npm run lint:staged", 18 | "test-server": "cd server && npm run test" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/helloukey/myMovies.git" 23 | }, 24 | "keywords": [ 25 | "mymovies", 26 | "movie", 27 | "app", 28 | "pwa", 29 | "movie", 30 | "app", 31 | "mern", 32 | "stack", 33 | "movie", 34 | "app" 35 | ], 36 | "author": "Kunal Ukey", 37 | "license": "ISC", 38 | "bugs": { 39 | "url": "https://github.com/helloukey/myMovies/issues" 40 | }, 41 | "homepage": "https://github.com/helloukey/myMovies#readme", 42 | "devDependencies": { 43 | "husky": "^9.0.11", 44 | "npm-run-all": "^4.1.5" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /server/controllers/collectionController.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Collection = require("../models/collection"); 3 | 4 | // All collection 5 | const allCollection = async (req, res) => { 6 | const id = req.user._id; 7 | try { 8 | const collection = await Collection.find({ userId: id }).sort({createdAt: -1}); 9 | res.status(200).json({collection}); 10 | } catch (error) { 11 | res.status(400).json({error: error.message}); 12 | } 13 | } 14 | 15 | // Add collection 16 | const addCollection = async (req, res) => { 17 | const { mediaId, title, mediaType, saveType, userId } = req.body; 18 | 19 | try { 20 | const singleItem = await Collection.create({mediaId, title, mediaType, saveType, userId}); 21 | res.status(200).json({singleItem}); 22 | } catch (error) { 23 | res.status(400).json({error: error.message}); 24 | } 25 | } 26 | 27 | // Delete collection 28 | const deleteCollection = async (req, res) => { 29 | const { id } = req.params; 30 | 31 | // check if id is valid 32 | if (!mongoose.Types.ObjectId.isValid(id)) { 33 | return res.status(404).json({error: "Invalid collection."}) 34 | } 35 | 36 | try { 37 | const singleItem = await Collection.findOneAndDelete({_id: id}); 38 | res.status(200).json({singleItem}); 39 | } catch (error) { 40 | res.status(400).json({error: error.message}); 41 | } 42 | } 43 | 44 | module.exports = { allCollection, addCollection, deleteCollection }; -------------------------------------------------------------------------------- /client/src/loaders/partials/Footer.js: -------------------------------------------------------------------------------- 1 | const Footer = () => { 2 | return ( 3 |
4 |
5 | {/* Copyright and Logo */} 6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | {/* Social Logos */} 15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | ); 25 | }; 26 | export default Footer; 27 | -------------------------------------------------------------------------------- /client/src/pages/shows/TrendingShows.js: -------------------------------------------------------------------------------- 1 | import BackNavigation from "../../components/general/BackNavigation"; 2 | import TrendingCategory from "../../components/general/Category"; 3 | import Footer from "../../components/general/Footer"; 4 | import { useGetShowTrendingPageQuery } from "../../redux/tmdbApi"; 5 | import { resetPage } from "../../redux/layoutSlice"; 6 | import { useSelector, useDispatch } from "react-redux"; 7 | import { useState, useEffect } from "react"; 8 | import useUniqueData from "../../hooks/useUniqueData"; 9 | import ListingScreen from "../../loaders/screens/ListingScreen"; 10 | 11 | const TrendingShows = () => { 12 | const page = useSelector(state => state.layout.page); 13 | const { data, isFetching, isLoading } = useGetShowTrendingPageQuery(page); 14 | const [final, setFinal] = useState([]); 15 | const dispatch = useDispatch(); 16 | const { uniqueItems } = useUniqueData(final); 17 | 18 | useEffect(() => { 19 | if(data && !isFetching) { 20 | setFinal(final => final.concat(...data.results)); 21 | } 22 | }, [data, isFetching]) 23 | 24 | // Reset to page 1 when unmounts 25 | useEffect(() => { 26 | return () => { 27 | dispatch(resetPage()) 28 | setFinal([]) 29 | } 30 | },[dispatch]) 31 | 32 | return ( 33 |
34 | {!isLoading && data ? 35 | <> 36 | 37 | 43 |
49 | ) 50 | } 51 | export default TrendingShows -------------------------------------------------------------------------------- /client/src/pages/movies/TrendingMovies.js: -------------------------------------------------------------------------------- 1 | import BackNavigation from "../../components/general/BackNavigation"; 2 | import TrendingCategory from "../../components/general/Category"; 3 | import Footer from "../../components/general/Footer"; 4 | import { useGetMovieTrendingPageQuery } from "../../redux/tmdbApi"; 5 | import { resetPage } from "../../redux/layoutSlice"; 6 | import { useSelector, useDispatch } from "react-redux"; 7 | import { useState, useEffect } from "react"; 8 | import useUniqueData from "../../hooks/useUniqueData"; 9 | import ListingScreen from "../../loaders/screens/ListingScreen"; 10 | 11 | const TrendingMovies = () => { 12 | const page = useSelector(state => state.layout.page); 13 | const { data, isFetching, isLoading } = useGetMovieTrendingPageQuery(page); 14 | const [final, setFinal] = useState([]); 15 | const dispatch = useDispatch(); 16 | const { uniqueItems } = useUniqueData(final); 17 | 18 | useEffect(() => { 19 | if(data && !isFetching) { 20 | setFinal(final => final.concat(...data.results)); 21 | } 22 | }, [data, isFetching]) 23 | 24 | // Reset to page 1 when unmounts 25 | useEffect(() => { 26 | return () => { 27 | dispatch(resetPage()) 28 | setFinal([]) 29 | } 30 | },[dispatch]) 31 | 32 | return ( 33 |
34 | {!isLoading && data ? 35 | <> 36 | 37 | 43 |
49 | ) 50 | } 51 | export default TrendingMovies -------------------------------------------------------------------------------- /client/src/pages/shows/PopularShows.js: -------------------------------------------------------------------------------- 1 | import BackNavigation from "../../components/general/BackNavigation"; 2 | import TrendingCategory from "../../components/general/Category"; 3 | import Footer from "../../components/general/Footer"; 4 | import { useGetShowPopularPageQuery } from "../../redux/tmdbApi"; 5 | import { resetPage } from "../../redux/layoutSlice"; 6 | import { useSelector, useDispatch } from "react-redux"; 7 | import { useState, useEffect } from "react"; 8 | import useUniqueData from "../../hooks/useUniqueData"; 9 | import ListingScreen from "../../loaders/screens/ListingScreen"; 10 | 11 | const PopularShows = () => { 12 | const page = useSelector(state => state.layout.page); 13 | const { data, isFetching, isLoading } = useGetShowPopularPageQuery(page); 14 | const [final, setFinal] = useState([]); 15 | const dispatch = useDispatch(); 16 | const { uniqueItems } = useUniqueData(final); 17 | 18 | useEffect(() => { 19 | if(data && !isFetching) { 20 | setFinal(final => final.concat(...data.results)); 21 | } 22 | }, [data, isFetching]) 23 | 24 | // Reset to page 1 when unmounts 25 | useEffect(() => { 26 | return () => { 27 | dispatch(resetPage()) 28 | setFinal([]) 29 | } 30 | },[dispatch]) 31 | 32 | return ( 33 |
34 | {!isLoading && data ? 35 | <> 36 | 37 | 44 |
50 | ) 51 | } 52 | export default PopularShows -------------------------------------------------------------------------------- /client/src/pages/movies/PopularMovies.js: -------------------------------------------------------------------------------- 1 | import BackNavigation from "../../components/general/BackNavigation"; 2 | import TrendingCategory from "../../components/general/Category"; 3 | import Footer from "../../components/general/Footer"; 4 | import { useGetMoviePopularPageQuery } from "../../redux/tmdbApi"; 5 | import { resetPage } from "../../redux/layoutSlice"; 6 | import { useSelector, useDispatch } from "react-redux"; 7 | import { useState, useEffect } from "react"; 8 | import useUniqueData from "../../hooks/useUniqueData"; 9 | import ListingScreen from "../../loaders/screens/ListingScreen"; 10 | 11 | const PopularMovies = () => { 12 | const page = useSelector(state => state.layout.page); 13 | const { data, isFetching, isLoading } = useGetMoviePopularPageQuery(page); 14 | const [final, setFinal] = useState([]); 15 | const dispatch = useDispatch(); 16 | const { uniqueItems } = useUniqueData(final); 17 | 18 | useEffect(() => { 19 | if(data && !isFetching) { 20 | setFinal(final => final.concat(...data.results)); 21 | } 22 | }, [data, isFetching]) 23 | 24 | // Reset to page 1 when unmounts 25 | useEffect(() => { 26 | return () => { 27 | dispatch(resetPage()) 28 | setFinal([]) 29 | } 30 | },[dispatch]) 31 | 32 | return ( 33 |
34 | {!isLoading && data ? 35 | <> 36 | 37 | 44 |
50 | ) 51 | } 52 | export default PopularMovies -------------------------------------------------------------------------------- /client/src/pages/shows/OnTheAirShows.js: -------------------------------------------------------------------------------- 1 | import BackNavigation from "../../components/general/BackNavigation"; 2 | import TrendingCategory from "../../components/general/Category"; 3 | import Footer from "../../components/general/Footer"; 4 | import { useGetShowOnTheAirPageQuery } from "../../redux/tmdbApi"; 5 | import { resetPage } from "../../redux/layoutSlice"; 6 | import { useSelector, useDispatch } from "react-redux"; 7 | import { useState, useEffect } from "react"; 8 | import useUniqueData from "../../hooks/useUniqueData"; 9 | import ListingScreen from "../../loaders/screens/ListingScreen"; 10 | 11 | const OnTheAirShows = () => { 12 | const page = useSelector(state => state.layout.page); 13 | const { data, isFetching, isLoading } = useGetShowOnTheAirPageQuery(page); 14 | const [final, setFinal] = useState([]); 15 | const dispatch = useDispatch(); 16 | const { uniqueItems } = useUniqueData(final); 17 | 18 | useEffect(() => { 19 | if(data && !isFetching) { 20 | setFinal(final => final.concat(...data.results)); 21 | } 22 | }, [data, isFetching]) 23 | 24 | // Reset to page 1 when unmounts 25 | useEffect(() => { 26 | return () => { 27 | dispatch(resetPage()) 28 | setFinal([]) 29 | } 30 | },[dispatch]) 31 | 32 | return ( 33 |
34 | {!isLoading && data ? 35 | <> 36 | 37 | 44 |
50 | ) 51 | } 52 | export default OnTheAirShows -------------------------------------------------------------------------------- /client/src/pages/shows/TopRatedShows.js: -------------------------------------------------------------------------------- 1 | import BackNavigation from "../../components/general/BackNavigation"; 2 | import TrendingCategory from "../../components/general/Category"; 3 | import Footer from "../../components/general/Footer"; 4 | import { useGetShowTopRatedPageQuery } from "../../redux/tmdbApi"; 5 | import { resetPage } from "../../redux/layoutSlice"; 6 | import { useSelector, useDispatch } from "react-redux"; 7 | import { useState, useEffect } from "react"; 8 | import useUniqueData from "../../hooks/useUniqueData"; 9 | import ListingScreen from "../../loaders/screens/ListingScreen"; 10 | 11 | const TopRatedShows = () => { 12 | const page = useSelector(state => state.layout.page); 13 | const { data, isFetching, isLoading } = useGetShowTopRatedPageQuery(page); 14 | const [final, setFinal] = useState([]); 15 | const dispatch = useDispatch(); 16 | const { uniqueItems } = useUniqueData(final); 17 | 18 | useEffect(() => { 19 | if(data && !isFetching) { 20 | setFinal(final => final.concat(...data.results)); 21 | } 22 | }, [data, isFetching]) 23 | 24 | // Reset to page 1 when unmounts 25 | useEffect(() => { 26 | return () => { 27 | dispatch(resetPage()) 28 | setFinal([]) 29 | } 30 | },[dispatch]) 31 | 32 | return ( 33 |
34 | {!isLoading && data ? 35 | <> 36 | 37 | 44 |
50 | ) 51 | } 52 | export default TopRatedShows -------------------------------------------------------------------------------- /client/src/pages/movies/NowPlayingMovies.js: -------------------------------------------------------------------------------- 1 | import BackNavigation from "../../components/general/BackNavigation"; 2 | import TrendingCategory from "../../components/general/Category"; 3 | import Footer from "../../components/general/Footer"; 4 | import { useGetMovieNowPlayingPageQuery } from "../../redux/tmdbApi"; 5 | import { resetPage } from "../../redux/layoutSlice"; 6 | import { useSelector, useDispatch } from "react-redux"; 7 | import { useState, useEffect } from "react"; 8 | import useUniqueData from "../../hooks/useUniqueData"; 9 | import ListingScreen from "../../loaders/screens/ListingScreen"; 10 | 11 | const NowPlayingMovies = () => { 12 | const page = useSelector((state) => state.layout.page); 13 | const { data, isFetching, isLoading } = useGetMovieNowPlayingPageQuery(page); 14 | const [final, setFinal] = useState([]); 15 | const dispatch = useDispatch(); 16 | const { uniqueItems } = useUniqueData(final); 17 | 18 | useEffect(() => { 19 | if (data && !isFetching) { 20 | setFinal((final) => final.concat(...data.results)); 21 | } 22 | }, [data, isFetching]); 23 | 24 | // Reset to page 1 when unmounts 25 | useEffect(() => { 26 | return () => { 27 | dispatch(resetPage()); 28 | setFinal([]); 29 | }; 30 | }, [dispatch]); 31 | 32 | return ( 33 |
34 | {!isLoading && data ? 35 | <> 36 | 37 | 44 |
50 | ); 51 | } 52 | export default NowPlayingMovies -------------------------------------------------------------------------------- /client/src/pages/shows/AiringTodayShows.js: -------------------------------------------------------------------------------- 1 | import BackNavigation from "../../components/general/BackNavigation"; 2 | import TrendingCategory from "../../components/general/Category"; 3 | import Footer from "../../components/general/Footer"; 4 | import { useGetShowAiringTodayPageQuery } from "../../redux/tmdbApi"; 5 | import { resetPage } from "../../redux/layoutSlice"; 6 | import { useSelector, useDispatch } from "react-redux"; 7 | import { useState, useEffect } from "react"; 8 | import useUniqueData from "../../hooks/useUniqueData"; 9 | import ListingScreen from "../../loaders/screens/ListingScreen"; 10 | 11 | const AiringTodayShows = () => { 12 | const page = useSelector(state => state.layout.page); 13 | const { data, isFetching, isLoading } = useGetShowAiringTodayPageQuery(page); 14 | const [final, setFinal] = useState([]); 15 | const dispatch = useDispatch(); 16 | const { uniqueItems } = useUniqueData(final); 17 | 18 | useEffect(() => { 19 | if(data && !isFetching) { 20 | setFinal(final => final.concat(...data.results)); 21 | } 22 | }, [data, isFetching]) 23 | 24 | // Reset to page 1 when unmounts 25 | useEffect(() => { 26 | return () => { 27 | dispatch(resetPage()) 28 | setFinal([]) 29 | } 30 | },[dispatch]) 31 | 32 | return ( 33 |
34 | {!isLoading && data ? 35 | <> 36 | 37 | 43 |
49 | ) 50 | } 51 | export default AiringTodayShows -------------------------------------------------------------------------------- /client/src/pages/movies/TopRatedMovies.js: -------------------------------------------------------------------------------- 1 | import BackNavigation from "../../components/general/BackNavigation"; 2 | import TrendingCategory from "../../components/general/Category"; 3 | import Footer from "../../components/general/Footer"; 4 | import { useGetMovieTopRatedPageQuery } from "../../redux/tmdbApi"; 5 | import { resetPage } from "../../redux/layoutSlice"; 6 | import { useSelector, useDispatch } from "react-redux"; 7 | import { useState, useEffect } from "react"; 8 | import useUniqueData from "../../hooks/useUniqueData"; 9 | import ListingScreen from "../../loaders/screens/ListingScreen"; 10 | 11 | const TopRatedMovies = () => { 12 | const page = useSelector(state => state.layout.page); 13 | const { data, isFetching, isLoading } = useGetMovieTopRatedPageQuery(page); 14 | const [final, setFinal] = useState([]); 15 | const dispatch = useDispatch(); 16 | const { uniqueItems } = useUniqueData(final); 17 | 18 | useEffect(() => { 19 | if(data && !isFetching) { 20 | setFinal(final => final.concat(...data.results)); 21 | } 22 | }, [data, isFetching]) 23 | 24 | // Reset to page 1 when unmounts 25 | useEffect(() => { 26 | return () => { 27 | dispatch(resetPage()) 28 | setFinal([]) 29 | } 30 | },[dispatch]) 31 | 32 | return ( 33 |
34 | {!isLoading && data ? 35 | <> 36 | 37 | 44 |
50 | ) 51 | } 52 | export default TopRatedMovies -------------------------------------------------------------------------------- /client/src/pages/movies/UpcomingMovies.js: -------------------------------------------------------------------------------- 1 | import BackNavigation from "../../components/general/BackNavigation"; 2 | import TrendingCategory from "../../components/general/Category"; 3 | import Footer from "../../components/general/Footer"; 4 | import { useGetMovieUpcomingPageQuery } from "../../redux/tmdbApi"; 5 | import { resetPage } from "../../redux/layoutSlice"; 6 | import { useSelector, useDispatch } from "react-redux"; 7 | import { useState, useEffect } from "react"; 8 | import useUniqueData from "../../hooks/useUniqueData"; 9 | import ListingScreen from "../../loaders/screens/ListingScreen"; 10 | 11 | const UpcomingMovies = () => { 12 | const page = useSelector(state => state.layout.page); 13 | const { data, isFetching, isLoading } = useGetMovieUpcomingPageQuery(page); 14 | const [final, setFinal] = useState([]); 15 | const dispatch = useDispatch(); 16 | const { uniqueItems } = useUniqueData(final); 17 | 18 | useEffect(() => { 19 | if(data && !isFetching) { 20 | setFinal(final => final.concat(...data.results)); 21 | } 22 | }, [data, isFetching]) 23 | 24 | // Reset to page 1 when unmounts 25 | useEffect(() => { 26 | return () => { 27 | dispatch(resetPage()) 28 | setFinal([]) 29 | } 30 | },[dispatch]) 31 | 32 | return ( 33 |
34 | {!isLoading && data ? 35 | <> 36 | 37 | 44 |
50 | ) 51 | } 52 | export default UpcomingMovies -------------------------------------------------------------------------------- /client/src/loaders/LazyImage.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from "react"; 2 | 3 | const LazyImage = ({ src, placeholder, type }) => { 4 | const [loading, setLoading] = useState(true); 5 | const [imageURL, setImageURL] = useState(""); 6 | const placeholderRef = useRef(); 7 | const imageRef = useRef(); 8 | 9 | useEffect(() => { 10 | const observer = new IntersectionObserver((entries) => { 11 | if (entries[0].isIntersecting) { 12 | setImageURL(imageRef?.current?.getAttribute("data-src")); 13 | } 14 | // unobserve on intersection 15 | if (entries[0].isIntersecting) observer.unobserve(entries[0].target); 16 | }); 17 | 18 | if(placeholderRef && placeholderRef.current) { 19 | observer.observe(placeholderRef.current); 20 | } 21 | 22 | }, [src, placeholder]); 23 | 24 | // check if image src has changed 25 | useEffect(() => { 26 | if (imageRef && imageRef.current && !loading) { 27 | if (imageRef.current.src !== src) { 28 | setImageURL(src); 29 | } 30 | } 31 | }, [src, imageRef, setImageURL, loading]) 32 | 33 | return ( 34 | <> 35 | {loading && 36 | 42 | } 43 | setLoading(false)} 56 | /> 57 | 58 | ); 59 | }; 60 | export default LazyImage; 61 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@react-icons/all-files": "^4.1.0", 7 | "@reduxjs/toolkit": "^1.8.6", 8 | "@testing-library/jest-dom": "^5.16.5", 9 | "@testing-library/react": "^13.4.0", 10 | "@testing-library/user-event": "^13.5.0", 11 | "daisyui": "^2.31.0", 12 | "react": "^18.2.0", 13 | "react-dom": "^18.2.0", 14 | "react-player": "^2.11.0", 15 | "react-redux": "^8.0.4", 16 | "react-router-dom": "^6.4.1", 17 | "react-scripts": "5.0.1", 18 | "web-vitals": "^2.1.4" 19 | }, 20 | "scripts": { 21 | "start": "react-scripts start", 22 | "build": "react-scripts build", 23 | "buildandserve": "react-scripts build && serve -s build", 24 | "lint:staged": "lint-staged" 25 | }, 26 | "lint-staged": { 27 | "src/**": "prettier --write --ignore-unknown", 28 | "src/**.js": "eslint --cache --fix --max-warnings=0", 29 | "**.js": "prettier --write" 30 | }, 31 | "browserslist": { 32 | "production": [ 33 | ">0.2%", 34 | "not dead", 35 | "not op_mini all" 36 | ], 37 | "development": [ 38 | "last 1 chrome version", 39 | "last 1 firefox version", 40 | "last 1 safari version" 41 | ] 42 | }, 43 | "devDependencies": { 44 | "@eslint/eslintrc": "^3.0.2", 45 | "@eslint/js": "^9.0.0", 46 | "autoprefixer": "^10.4.12", 47 | "cypress": "^13.7.3", 48 | "eslint": "^8.57.0", 49 | "eslint-config-prettier": "^9.1.0", 50 | "eslint-plugin-cypress": "^2.15.1", 51 | "eslint-plugin-react": "^7.34.1", 52 | "globals": "^15.0.0", 53 | "lint-staged": "^15.2.2", 54 | "postcss": "^8.4.16", 55 | "prettier": "3.2.5", 56 | "tailwindcss": "^3.1.8" 57 | }, 58 | "proxy": "http://localhost:8000" 59 | } 60 | -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | myMovies 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /client/cypress/helpers/checkFooter.js: -------------------------------------------------------------------------------- 1 | const checkFooter = () => { 2 | it("Check the footer", () => { 3 | cy.getCypress("footer-container").should("exist"); 4 | cy.getCypress("footer-container").should("be.visible"); 5 | cy.getCypress("footer-text-container").should("exist"); 6 | cy.getCypress("footer-text-container").should("be.visible"); 7 | cy.getCypress("footer-copyright-text").should("exist"); 8 | cy.getCypress("footer-copyright-text").should("be.visible"); 9 | cy.getCypress("footer-copyright-text").should( 10 | "contain.text", 11 | "Copyright ©" 12 | ); 13 | cy.getCypress("footer-made-by-text").should("exist"); 14 | cy.getCypress("footer-made-by-text").should("be.visible"); 15 | cy.getCypress("footer-made-by-text").should( 16 | "contain.text", 17 | "Made with ❤ @helloukey" 18 | ); 19 | cy.getCypress("footer-data-by-text").should("exist"); 20 | cy.getCypress("footer-data-by-text").should("be.visible"); 21 | cy.getCypress("footer-data-by-text").should( 22 | "contain.text", 23 | "Data provided by - TMDB" 24 | ); 25 | cy.getCypress("footer-social-links-container").should("exist"); 26 | cy.getCypress("footer-social-links-container").should("be.visible"); 27 | cy.getCypress("footer-website-icon").should("exist"); 28 | cy.getCypress("footer-website-icon").should("be.visible"); 29 | cy.getCypress("footer-github-icon").should("exist"); 30 | cy.getCypress("footer-github-icon").should("be.visible"); 31 | cy.getCypress("footer-linkedin-icon").should("exist"); 32 | cy.getCypress("footer-linkedin-icon").should("be.visible"); 33 | cy.getCypress("footer-youtube-icon").should("exist"); 34 | cy.getCypress("footer-youtube-icon").should("be.visible"); 35 | cy.getCypress("footer-mail-icon").should("exist"); 36 | cy.getCypress("footer-mail-icon").should("be.visible"); 37 | }); 38 | }; 39 | 40 | export { checkFooter }; 41 | -------------------------------------------------------------------------------- /client/src/pages/movies/MovieGenre.js: -------------------------------------------------------------------------------- 1 | import BackNavigation from "../../components/general/BackNavigation"; 2 | import MovieGenreCategory from "../../components/general/Category"; 3 | import Footer from "../../components/general/Footer"; 4 | import { useGetMovieGenrePageQuery } from "../../redux/tmdbApi"; 5 | import { resetPage } from "../../redux/layoutSlice"; 6 | import { useSelector, useDispatch } from "react-redux"; 7 | import { useState, useEffect } from "react"; 8 | import { useParams } from "react-router-dom"; 9 | import useUniqueData from "../../hooks/useUniqueData"; 10 | import ListingScreen from "../../loaders/screens/ListingScreen"; 11 | 12 | const MovieGenre = () => { 13 | const { id } = useParams(); 14 | const genreHeading = useSelector(state => state.genre.genres.filter(genre => genre.id === parseInt(id))[0]); 15 | const page = useSelector(state => state.layout.page); 16 | const { data, isFetching, isLoading } = useGetMovieGenrePageQuery({page: page, genre: id}); 17 | const [final, setFinal] = useState([]); 18 | const dispatch = useDispatch(); 19 | const { uniqueItems } = useUniqueData(final); 20 | 21 | useEffect(() => { 22 | if(data && !isFetching) { 23 | setFinal(final => final.concat(...data.results)); 24 | } 25 | }, [data, isFetching]) 26 | 27 | // Reset to page 1 when unmounts 28 | useEffect(() => { 29 | return () => { 30 | dispatch(resetPage()) 31 | setFinal([]) 32 | } 33 | },[dispatch]) 34 | 35 | return ( 36 |
37 | {!isLoading && data ? 38 | <> 39 | 40 | 46 |
52 | ) 53 | } 54 | export default MovieGenre -------------------------------------------------------------------------------- /client/src/components/user/UserListing.js: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | 3 | const UserListing = ({ data, saveType }) => { 4 | return ( 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | {data && 16 | data 17 | .filter((item) => item.saveType.includes(saveType)) 18 | .map((item) => ( 19 | 23 | 24 | 40 | 43 | 44 | ))} 45 | 46 |
IDNAMESAVE TYPE
{item.mediaId} 25 | 37 | {item.title} 38 | 39 | 41 | {item.saveType} 42 |
47 |
48 | ); 49 | }; 50 | export default UserListing; 51 | -------------------------------------------------------------------------------- /client/src/pages/shows/ShowGenre.js: -------------------------------------------------------------------------------- 1 | import BackNavigation from "../../components/general/BackNavigation"; 2 | import ShowGenreCategory from "../../components/general/Category"; 3 | import Footer from "../../components/general/Footer"; 4 | import { useGetShowGenrePageQuery } from "../../redux/tmdbApi"; 5 | import { resetPage } from "../../redux/layoutSlice"; 6 | import { useSelector, useDispatch } from "react-redux"; 7 | import { useState, useEffect } from "react"; 8 | import { useParams } from "react-router-dom"; 9 | import useUniqueData from "../../hooks/useUniqueData"; 10 | import ListingScreen from "../../loaders/screens/ListingScreen"; 11 | 12 | const ShowGenre = () => { 13 | const { id } = useParams(); 14 | const genreHeading = useSelector( 15 | (state) => 16 | state.genre.genres.filter((genre) => genre.id === parseInt(id))[0] 17 | ); 18 | const page = useSelector((state) => state.layout.page); 19 | const { data, isFetching, isLoading } = useGetShowGenrePageQuery({ 20 | page: page, 21 | genre: id, 22 | }); 23 | const [final, setFinal] = useState([]); 24 | const dispatch = useDispatch(); 25 | const { uniqueItems } = useUniqueData(final); 26 | 27 | useEffect(() => { 28 | if (data && !isFetching) { 29 | setFinal((final) => final.concat(...data.results)); 30 | } 31 | }, [data, isFetching]); 32 | 33 | // Reset to page 1 when unmounts 34 | useEffect(() => { 35 | return () => { 36 | dispatch(resetPage()); 37 | setFinal([]); 38 | }; 39 | }, [dispatch]); 40 | 41 | return ( 42 |
43 | {!isLoading && data ? 44 | <> 45 | 46 | 52 |
58 | ); 59 | } 60 | export default ShowGenre -------------------------------------------------------------------------------- /client/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | * { 6 | box-sizing: border-box; 7 | margin: 0; 8 | padding: 0; 9 | } 10 | 11 | html { 12 | scroll-behavior: smooth; 13 | } 14 | 15 | body { 16 | width: 100%; 17 | min-height: 100vh; 18 | background-color: #111; 19 | } 20 | 21 | .tab.tab-active { 22 | background-color: #333; 23 | } 24 | 25 | .space-x-4 > :not([hidden]) ~ :not([hidden]) { 26 | --tw-space-x-reverse: 0; 27 | margin-right: calc(1rem * var(--tw-space-x-reverse)); 28 | margin-left: 0px; 29 | } 30 | 31 | .player-wrapper { 32 | position: relative; 33 | padding-top: 56.25%; 34 | } 35 | 36 | .react-player { 37 | position: absolute; 38 | top: 0; 39 | left: 0; 40 | } 41 | 42 | .player-wrapper-hidden { 43 | display: none; 44 | } 45 | 46 | @media (min-width: 1024px) { 47 | .react-player__play-icon { 48 | display: none; 49 | } 50 | } 51 | 52 | /* SearchBar */ 53 | .searchBar { 54 | animation-name: appearSearchBar; 55 | animation-duration: 300ms; 56 | animation-timing-function: ease-in-out; 57 | } 58 | 59 | @keyframes appearSearchBar { 60 | from { 61 | transform: translateY(-100px); 62 | } 63 | to { 64 | transform: translateY(0px); 65 | } 66 | } 67 | 68 | /* fadeImage */ 69 | .fadeImage { 70 | animation-name: fadingImage; 71 | animation-duration: 300ms; 72 | animation-timing-function: ease-in-out; 73 | } 74 | 75 | @keyframes fadingImage { 76 | from { 77 | opacity: 0; 78 | } 79 | to { 80 | opacity: 1; 81 | } 82 | } 83 | 84 | /* fadeHeroText */ 85 | .fadeHeroText { 86 | animation-name: fadingHeroText; 87 | animation-duration: 1000ms; 88 | animation-timing-function: ease-in-out; 89 | } 90 | 91 | @keyframes fadingHeroText { 92 | from { 93 | opacity: 0; 94 | transform: translateY(25px); 95 | } 96 | to { 97 | opacity: 1; 98 | transform: translateY(0px); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /client/src/assets/EmptyHero.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # myMovies 3 | 4 | myMovies is a complete MERN stack progressive web app (PWA) for everything related to Movies, Shows, and Artists. The movie database is taken from my [TMDB](https://www.themoviedb.org/) API and built using technologies including [ReactJS](https://reactjs.org/), [ReduxJS/Toolkit](https://redux-toolkit.js.org/), [TailwindCSS](https://tailwindcss.com/), [DaisyUI](https://daisyui.com/), [NodeJS](https://nodejs.org/), [ExpressJS](https://expressjs.com/), and [MongoDB](https://www.mongodb.com/). 5 | 6 | ## Features 7 | 8 | - Progessive Web App (PWA) 9 | - Search Movies, Shows, and Artists 10 | - Complete Details of Movies, Shows, and Artists 11 | - Watch Trailers 12 | - Browse Backdrops and Posters 13 | - Add to Liked and Watch Later Collection 14 | 15 | ## Preview 16 | 17 | ![preview-mymovies](https://user-images.githubusercontent.com/43317360/206182064-de4727e2-20d3-4609-8faa-93d0795ff7dc.jpg) 18 | 19 | ## Installation & Setup 20 | 21 | - First, download or clone this repo, and then run the command given below to install all the required dependencies. 22 | 23 | ```bash 24 | npm install-client && install-server 25 | ``` 26 | 27 | - Rename the `.env_sample` file to `.env` inside `server` folder. 28 | 29 | - Get TMDB API Key from **[HERE](https://developers.themoviedb.org/3)** and MongoDB connection URI from **[HERE](https://www.mongodb.com/)** 30 | 31 | - Provide your **MONGODB_URI**, **JWT_SECRET_KEY**, **PORT**, **BASE_URL**, and **API_KEY** inside the `.env` file. 32 | 33 | - Rename the `.env_sample` file to `.env` inside `client` folder. 34 | 35 | - Provide your **REACT_APP_BACKEND_URL** inside the `.env` file. 36 | 37 | - Run `npm start-server && start-client` from the root folder. 38 | 39 | - Finally, Preview this project locally by visiting the URL: `localhost:` 40 | 41 | ## Docker Compose 42 | 43 | - Run the project in docker container using: 44 | ```docker compose up -d --build``` 45 | - Stop the docker container using: 46 | ```docker compose down``` 47 | 48 | ## License 49 | 50 | [![license](https://img.shields.io/github/license/helloukey/mymovies?style=for-the-badge)](LICENSE) -------------------------------------------------------------------------------- /client/src/components/user/UserProfile.js: -------------------------------------------------------------------------------- 1 | import LazyImage from "../../loaders/LazyImage"; 2 | import LoadingHero from "../../assets/LoadingHero.svg"; 3 | import UserTabs from "./UserTabs"; 4 | import { removeUser } from "../../redux/userSlice"; 5 | import { useDispatch } from "react-redux"; 6 | import userHero from "../../assets/userHero.svg"; 7 | 8 | const UserProfile = ({ heading, token }) => { 9 | const dispatch = useDispatch(); 10 | 11 | return ( 12 |
13 |
17 | {/* Hero Image */} 18 |
19 | 20 |
21 | 22 | {/* Typography */} 23 |
27 | {/* Heading */} 28 |

32 | Hi, {heading} 33 |

34 | 35 | {/* Description */} 36 |

40 | Welcome to my-Movies! 41 |
42 | Your own personal collection store. 43 |

44 | {/* Logout */} 45 | 52 |
53 |
54 | 55 | {/* UserTabs */} 56 | 57 |
58 | ); 59 | }; 60 | export default UserProfile; 61 | -------------------------------------------------------------------------------- /server/.eslintcache: -------------------------------------------------------------------------------- 1 | [{"D:\\Projects\\myMovies\\server\\__test__\\db.js":"1","D:\\Projects\\myMovies\\server\\__test__\\tmdb\\tmdb.test.js":"2","D:\\Projects\\myMovies\\server\\app.js":"3","D:\\Projects\\myMovies\\server\\server.js":"4","D:\\Projects\\myMovies\\server\\__test__\\user\\user.test.js":"5","D:\\Projects\\myMovies\\server\\__test__\\collection\\collection.test.js":"6","D:\\Projects\\myMovies\\server\\__test__\\helpers\\createUser.js":"7"},{"size":1090,"mtime":1713522569885,"results":"8","hashOfConfig":"9"},{"size":17002,"mtime":1713522570066,"results":"10","hashOfConfig":"11"},{"size":846,"mtime":1713522570060,"results":"12","hashOfConfig":"9"},{"size":357,"mtime":1713522570079,"results":"13","hashOfConfig":"9"},{"size":5713,"mtime":1713528816090,"results":"14","hashOfConfig":"11"},{"size":11158,"mtime":1713543986095,"results":"15","hashOfConfig":"11"},{"size":240,"mtime":1713543986121,"results":"16","hashOfConfig":"9"},{"filePath":"17","messages":"18","suppressedMessages":"19","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"14v3ozv",{"filePath":"20","messages":"21","suppressedMessages":"22","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"pgo0rv",{"filePath":"23","messages":"24","suppressedMessages":"25","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"26","messages":"27","suppressedMessages":"28","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"29","messages":"30","suppressedMessages":"31","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"32","messages":"33","suppressedMessages":"34","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"35","messages":"36","suppressedMessages":"37","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"D:\\Projects\\myMovies\\server\\__test__\\db.js",[],[],"D:\\Projects\\myMovies\\server\\__test__\\tmdb\\tmdb.test.js",[],[],"D:\\Projects\\myMovies\\server\\app.js",[],[],"D:\\Projects\\myMovies\\server\\server.js",[],[],"D:\\Projects\\myMovies\\server\\__test__\\user\\user.test.js",[],[],"D:\\Projects\\myMovies\\server\\__test__\\collection\\collection.test.js",[],[],"D:\\Projects\\myMovies\\server\\__test__\\helpers\\createUser.js",[],[]] -------------------------------------------------------------------------------- /client/src/pages/SearchResults.js: -------------------------------------------------------------------------------- 1 | import SearchCategory from "../components/general/Category"; 2 | import Footer from "../components/general/Footer"; 3 | import { useGetSearchResultsQuery } from "../redux/tmdbApi"; 4 | import { resetPage } from "../redux/layoutSlice"; 5 | import { useSelector, useDispatch } from "react-redux"; 6 | import { useState, useEffect } from "react"; 7 | import useUniqueData from "../hooks/useUniqueData"; 8 | import { useSearchParams } from "react-router-dom"; 9 | import ListingScreen from "../loaders/screens/ListingScreen"; 10 | 11 | const SearchResults = () => { 12 | const { isSearch, page } = useSelector((state) => state.layout); 13 | const [searchParam] = useSearchParams(); 14 | const [waiting, setWaiting] = useState(true); 15 | 16 | const { data, isFetching, isLoading } = useGetSearchResultsQuery({ 17 | query: searchParam.get("q"), 18 | page: page, 19 | }, {skip: waiting}); 20 | const [final, setFinal] = useState([]); 21 | const dispatch = useDispatch(); 22 | const { uniqueItems } = useUniqueData(final); 23 | 24 | // Debounce fetch 25 | useEffect(() => { 26 | setWaiting(true); 27 | const handler = setTimeout(() => { 28 | setWaiting(false); 29 | }, 500); 30 | 31 | return () => clearTimeout(handler); 32 | },[searchParam]) 33 | 34 | useEffect(() => { 35 | if (data && !isFetching) { 36 | setFinal((final) => final.concat(...data.results)); 37 | } 38 | }, [data, isFetching]); 39 | 40 | // Reset to page 1 when unmounts 41 | useEffect(() => { 42 | return () => { 43 | dispatch(resetPage()); 44 | setFinal([]); 45 | }; 46 | }, [dispatch]); 47 | 48 | // Reset Page && Clear data on searchQuery 49 | useEffect(() => { 50 | if (searchParam) { 51 | setFinal([]); 52 | dispatch(resetPage()); 53 | } 54 | }, [searchParam, dispatch]); 55 | 56 | return ( 57 |
60 | {!isLoading && data 61 | ? 62 | <> 63 | 71 |
72 | 73 | : 74 | 75 | } 76 |
77 | ); 78 | }; 79 | export default SearchResults; 80 | -------------------------------------------------------------------------------- /client/src/pages/shows/Shows.js: -------------------------------------------------------------------------------- 1 | import Hero from "../../components/general/Hero"; 2 | import ShowsAiringToday from "../../components/carousel/Carousel"; 3 | import PopularShows from "../../components/carousel/Carousel"; 4 | import TopRatedShows from "../../components/carousel/Carousel"; 5 | import ShowsOnAir from "../../components/carousel/Carousel"; 6 | import Footer from "../../components/general/Footer"; 7 | import { 8 | useGetSingleTrendingQuery, 9 | useGetShowListQuery, 10 | } from "../../redux/tmdbApi"; 11 | import MainScreen from "../../loaders/screens/MainScreen"; 12 | 13 | const Shows = () => { 14 | const { data, isLoading } = useGetSingleTrendingQuery("tv"); 15 | const { 16 | data: nowPlaying, 17 | isLoading: nowPlayingLoading, 18 | } = useGetShowListQuery("airing_today"); 19 | const { 20 | data: popular, 21 | isLoading: popularLoading, 22 | } = useGetShowListQuery("popular"); 23 | const { 24 | data: topRated, 25 | isLoading: topRatedLoading, 26 | } = useGetShowListQuery("top_rated"); 27 | const { 28 | data: onAirShows, 29 | isLoading: onAirShowsLoading, 30 | } = useGetShowListQuery("on_the_air"); 31 | return ( 32 |
33 | {!isLoading && 34 | !nowPlayingLoading && 35 | !popularLoading && 36 | !topRatedLoading && 37 | !onAirShowsLoading && 38 | data && 39 | nowPlaying && 40 | popular && 41 | topRated && 42 | onAirShows ? ( 43 | <> 44 | 45 | 51 | 57 | 63 | 69 |
70 | 71 | ) : ( 72 | 73 | )} 74 |
75 | ); 76 | }; 77 | export default Shows; 78 | -------------------------------------------------------------------------------- /client/src/pages/movies/Movies.js: -------------------------------------------------------------------------------- 1 | import Hero from "../../components/general/Hero"; 2 | import MoviesNowPlaying from "../../components/carousel/Carousel"; 3 | import PopularMovies from "../../components/carousel/Carousel"; 4 | import TopRatedMovies from "../../components/carousel/Carousel"; 5 | import UpcomingMovies from "../../components/carousel/Carousel"; 6 | import Footer from "../../components/general/Footer"; 7 | import { 8 | useGetSingleTrendingQuery, 9 | useGetMovieListQuery, 10 | } from "../../redux/tmdbApi"; 11 | import MainScreen from "../../loaders/screens/MainScreen"; 12 | 13 | const Movies = () => { 14 | const { data, isLoading, error } = useGetSingleTrendingQuery("movie"); 15 | const { 16 | data: nowPlaying, 17 | isLoading: nowPlayingLoading, 18 | } = useGetMovieListQuery("now_playing"); 19 | const { 20 | data: popular, 21 | isLoading: popularLoading, 22 | } = useGetMovieListQuery("popular"); 23 | const { 24 | data: topRated, 25 | isLoading: topRatedLoading, 26 | } = useGetMovieListQuery("top_rated"); 27 | const { 28 | data: upcoming, 29 | isLoading: upcomingLoading, 30 | } = useGetMovieListQuery("upcoming"); 31 | return ( 32 |
33 | {!isLoading && 34 | !nowPlayingLoading && 35 | !popularLoading && 36 | !topRatedLoading && 37 | !upcomingLoading && 38 | data && 39 | nowPlaying && 40 | popular && 41 | topRated && 42 | upcoming ? ( 43 | <> 44 | 45 | 51 | 57 | 63 | 69 |
70 | 71 | ) : ( 72 | 73 | )} 74 |
75 | ); 76 | }; 77 | export default Movies; 78 | -------------------------------------------------------------------------------- /server/routes/tmdb.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | 4 | // Route controller 5 | const { getTrending, singleMovieTrending, singleShowTrending, getMovieList, getShowList, getMovieTrendingPage, getMovieNowPlayingPage, getMoviePopularPage, getMovieTopRatedPage, getMovieUpcomingPage, getMovieDetailsPage, getMovieGenrePage, getShowTrendingPage, getShowAiringTodayPage, getShowPopularPage, getShowTopRatedPage, getShowOnTheAirPage, getShowDetailsPage, getShowEpisodesPage, getShowGenrePage, getPersonDetailsPage, getSearchResults } = require("../controllers/tmdbController"); 6 | 7 | // get Trending 8 | router.get("/trending/:type", getTrending) 9 | 10 | // single Trending Movie 11 | router.get("/trending/movie/:id", singleMovieTrending) 12 | 13 | // single Trending Show 14 | router.get("/trending/show/:id", singleShowTrending) 15 | 16 | // get Movie List 17 | router.get("/list/movie/:type", getMovieList) 18 | 19 | // get Show List 20 | router.get("/list/show/:type", getShowList) 21 | 22 | // get Trending Movie Page 23 | router.get("/trending/movie/page/:page", getMovieTrendingPage) 24 | 25 | // get Now Playing Movie Page 26 | router.get("/movie/nowplaying/:page", getMovieNowPlayingPage) 27 | 28 | // get Popular Movie Page 29 | router.get("/movie/popular/:page", getMoviePopularPage) 30 | 31 | // get Top-Rated Movie Page 32 | router.get("/movie/toprated/:page", getMovieTopRatedPage) 33 | 34 | // get Upcoming Movie Page 35 | router.get("/movie/upcoming/:page", getMovieUpcomingPage) 36 | 37 | // get Movie Details Page 38 | router.get("/details/movie/:id", getMovieDetailsPage) 39 | 40 | // get Movie Genre Page 41 | router.get("/movie/genre/:genre/:page", getMovieGenrePage) 42 | 43 | // get Trending Show Page 44 | router.get("/trending/show/page/:page", getShowTrendingPage) 45 | 46 | // get Airing Today Show Page 47 | router.get("/show/airingtoday/:page", getShowAiringTodayPage) 48 | 49 | // get Popular Show Page 50 | router.get("/show/popular/:page", getShowPopularPage) 51 | 52 | // get Top-Rated Show Page 53 | router.get("/show/toprated/:page", getShowTopRatedPage) 54 | 55 | // get On The Air Show Page 56 | router.get("/show/ontheair/:page", getShowOnTheAirPage) 57 | 58 | // get Show Details Page 59 | router.get("/details/show/:id", getShowDetailsPage) 60 | 61 | // get Show Episodes Details Page 62 | router.get("/episodes/show/:id/:seasonnumber", getShowEpisodesPage) 63 | 64 | // get Show Genre Page 65 | router.get("/show/genre/:genre/:page", getShowGenrePage) 66 | 67 | // get Person Details Page 68 | router.get("/person/:id", getPersonDetailsPage) 69 | 70 | // get Search Page 71 | router.get("/search/:query/:page", getSearchResults) 72 | 73 | module.exports = router; -------------------------------------------------------------------------------- /client/src/components/rating/CardRating.js: -------------------------------------------------------------------------------- 1 | const CardRating = ({ item }) => { 2 | return ( 3 | <> 4 | {item.vote_average > 0 ? 5 |
6 | 0 ? true : false 14 | } 15 | /> 16 | 2 ? true : false 24 | } 25 | /> 26 | 5 ? true : false 34 | } 35 | /> 36 | 7 ? true : false 44 | } 45 | /> 46 | 9 || item.vote_average === 0 ? true : false 54 | } 55 | /> 56 |
57 | : 58 |
59 | 60 | 61 | 62 | 63 | 64 | 65 |
66 | } 67 | 68 | ); 69 | }; 70 | export default CardRating; 71 | -------------------------------------------------------------------------------- /client/src/components/artist/department/WritingDepartment.js: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | 3 | const WritingDepartment = ({mediaFilter, data}) => { 4 | return ( 5 | <> 6 |

Writing

7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | {data && 16 | data.crew 17 | .filter( 18 | (item) => 19 | item.department === "Writing" && 20 | item.media_type.includes(mediaFilter) 21 | ) 22 | .map((item) => ( 23 | 27 | {item.release_date || item.first_air_date ? ( 28 | 35 | ) : ( 36 | 37 | )} 38 | 39 | 62 | 63 | ))} 64 | 65 |
YEARNAME
29 | {item.release_date 30 | ? new Date(item.release_date).getFullYear() 31 | : item.first_air_date 32 | ? new Date(item.first_air_date).getFullYear() 33 | : ""} 34 | - 40 | 51 | 52 | {item.title || 53 | item.original_title || 54 | item.name || 55 | item.original_name}{" "} 56 | 57 | {item.job && ( 58 | as {item.job} 59 | )} 60 | 61 |
66 | 67 | ); 68 | }; 69 | export default WritingDepartment; 70 | -------------------------------------------------------------------------------- /client/src/components/artist/department/ProductionDepartment.js: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | 3 | const ProductionDepartment = ({ mediaFilter, data }) => { 4 | return ( 5 | <> 6 |

Production

7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | {data && 16 | data.crew 17 | .filter( 18 | (item) => 19 | item.department === "Production" && 20 | item.media_type.includes(mediaFilter) 21 | ) 22 | .map((item) => ( 23 | 27 | {item.release_date || item.first_air_date ? ( 28 | 35 | ) : ( 36 | 37 | )} 38 | 39 | 62 | 63 | ))} 64 | 65 |
YEARNAME
29 | {item.release_date 30 | ? new Date(item.release_date).getFullYear() 31 | : item.first_air_date 32 | ? new Date(item.first_air_date).getFullYear() 33 | : ""} 34 | - 40 | 51 | 52 | {item.title || 53 | item.original_title || 54 | item.name || 55 | item.original_name}{" "} 56 | 57 | {item.job && ( 58 | as {item.job} 59 | )} 60 | 61 |
66 | 67 | ); 68 | }; 69 | export default ProductionDepartment; 70 | -------------------------------------------------------------------------------- /client/src/components/artist/department/DirectingDepartment.js: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | 3 | const DirectingDepartment = ({ mediaFilter, data }) => { 4 | return ( 5 | <> 6 |

Directing

7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | {data && 16 | data.crew 17 | .filter( 18 | (item) => 19 | item.department === "Directing" && 20 | item.media_type.includes(mediaFilter) 21 | ) 22 | .map((item) => ( 23 | 27 | {item.release_date || item.first_air_date ? ( 28 | 35 | ) : ( 36 | 37 | )} 38 | 39 | 62 | 63 | ))} 64 | 65 |
YEARNAME
29 | {item.release_date 30 | ? new Date(item.release_date).getFullYear() 31 | : item.first_air_date 32 | ? new Date(item.first_air_date).getFullYear() 33 | : ""} 34 | - 40 | 51 | 52 | {item.title || 53 | item.original_title || 54 | item.name || 55 | item.original_name}{" "} 56 | 57 | {item.job && ( 58 | as {item.job} 59 | )} 60 | 61 |
66 | 67 | ); 68 | }; 69 | export default DirectingDepartment; 70 | -------------------------------------------------------------------------------- /server/models/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | const bcrypt = require("bcrypt"); 4 | const validator = require("validator"); 5 | 6 | const userSchema = new Schema( 7 | { 8 | firstName: { 9 | type: String, 10 | required: [true, "First Name is required."], 11 | minlength: [3, "First Name should of minimum 3 characters."], 12 | maxlength: [12, "First Name can be of maximum 12 characters only."], 13 | }, 14 | email: { 15 | type: String, 16 | required: [true, "Email is required."], 17 | unique: true, 18 | }, 19 | password: { 20 | type: String, 21 | required: [true, "Password is required."], 22 | }, 23 | }, 24 | { timestamps: true } 25 | ); 26 | 27 | // Register static method 28 | userSchema.statics.register = async function (firstName, email, password) { 29 | // check if all fields are provided 30 | if (!firstName || !email || !password) { 31 | throw Error("All fields must be provided."); 32 | } 33 | 34 | // check if first name length is more than 3 characters 35 | if (firstName.length < 3) { 36 | throw Error("First name should be of at least 3 characters."); 37 | } 38 | // check if email is valid 39 | if (!validator.isEmail(email)) { 40 | throw Error("Please enter a valid email address."); 41 | } 42 | // check if password is strong 43 | if (!validator.isStrongPassword) { 44 | throw Error("Please provide a strong password."); 45 | } 46 | 47 | // check if email already exists 48 | const account = await this.findOne({ email }); 49 | 50 | // Throw an error if the email already exists 51 | if (account) { 52 | throw Error("Email already registered."); 53 | } 54 | 55 | // generate hashed password 56 | const salt = await bcrypt.genSalt(10); 57 | const hash = await bcrypt.hash(password, salt); 58 | 59 | const user = await this.create({ firstName, email, password: hash }); 60 | return user; 61 | }; 62 | 63 | // Login static method 64 | userSchema.statics.login = async function (email, password) { 65 | // check if all fields are provided 66 | if (!email || !password) { 67 | throw Error("All fields must be provided."); 68 | } 69 | 70 | // check if email already exists 71 | const user = await this.findOne({ email }); 72 | 73 | // Throw an error if the email does not exists 74 | if (!user) { 75 | throw Error("This email is not registered."); 76 | } 77 | 78 | // compare password 79 | const comparedPassword = await bcrypt.compare(password, user.password); 80 | 81 | // check if password is correct 82 | if (!comparedPassword) { 83 | throw Error("Incorrect password."); 84 | } 85 | return user; 86 | }; 87 | 88 | module.exports = mongoose.model("User", userSchema); 89 | -------------------------------------------------------------------------------- /client/public/offline.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | myMovies - You are offline! 8 | 9 | 10 | 69 | 70 | 71 | 72 |
73 |

Oops!

74 |

It seems there is a problem with your network connection.
75 | Please check your network status. 76 |

77 |
78 | 79 | 80 |
81 |
82 | 83 | 84 | 85 | 98 | -------------------------------------------------------------------------------- /client/src/components/artist/department/ActingDepartment.js: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | 3 | const ActingDepartment = ({mediaFilter, data}) => { 4 | return ( 5 | <> 6 |

Acting

7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | {data && 16 | data.cast && 17 | data.cast 18 | .filter((item) => item.media_type.includes(mediaFilter)) 19 | .map((item) => ( 20 | 24 | {item.release_date || item.first_air_date ? ( 25 | 32 | ) : ( 33 | 34 | )} 35 | 63 | 64 | ))} 65 | 66 |
YEARNAME
26 | {item.release_date 27 | ? new Date(item.release_date).getFullYear() 28 | : item.first_air_date 29 | ? new Date(item.first_air_date).getFullYear() 30 | : ""} 31 | - 36 | 47 | 48 | {item.title || 49 | item.original_title || 50 | item.name || 51 | item.original_name}{" "} 52 | 53 | {item.episode_count && ( 54 | 55 | ({item.episode_count} episodes){" "} 56 | 57 | )} 58 | {item.character && ( 59 | as {item.character} 60 | )} 61 | 62 |
67 | 68 | ); 69 | }; 70 | export default ActingDepartment; 71 | -------------------------------------------------------------------------------- /client/src/components/details/DetailsEpisodes.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import LazyImage from "../../loaders/LazyImage"; 3 | import EmptyHero from "../../assets/EmptyHero.svg"; 4 | import LoadingHero from "../../assets/LoadingHero.svg"; 5 | import { useSelector } from "react-redux"; 6 | import { useGetShowEpisodesQuery } from "../../redux/tmdbApi"; 7 | 8 | const DetailsEpisodes = ({ seasons }) => { 9 | const [seasonFilter, setSeasonFilter] = useState(1); 10 | const { data } = useGetShowEpisodesQuery({ 11 | show_id: seasons?.id, 12 | seasonNumber: seasonFilter, 13 | }); 14 | const backdrop = useSelector((state) => state.layout.backdrop); 15 | 16 | const handleFilter = (e) => { 17 | setSeasonFilter(e.target.value); 18 | }; 19 | 20 | useEffect(() => { 21 | if (seasons && seasons.seasons[0].season_number === 0) { 22 | setSeasonFilter(0); 23 | } 24 | }, [seasons]); 25 | 26 | return ( 27 |
28 | {/* Heading */} 29 |

33 | Episodes 34 |

35 | 36 | {seasons && seasons.seasons && ( 37 | 51 | )} 52 | 53 | {/* Container */} 54 |
58 | {data && 59 | data.episodes && 60 | data.episodes.map((item) => ( 61 |
66 | 71 |
72 |

73 | E{item.episode_number} 74 |

75 |

{item.name}

76 |

{item.overview}

77 |
78 |
79 | ))} 80 |
81 |
82 | ); 83 | }; 84 | export default DetailsEpisodes; 85 | -------------------------------------------------------------------------------- /client/src/redux/genreSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit' 2 | 3 | const initialState = { 4 | genres: [ 5 | { 6 | "id": 28, 7 | "name": "Action" 8 | }, 9 | { 10 | "id": 12, 11 | "name": "Adventure" 12 | }, 13 | { 14 | "id": 16, 15 | "name": "Animation" 16 | }, 17 | { 18 | "id": 35, 19 | "name": "Comedy" 20 | }, 21 | { 22 | "id": 80, 23 | "name": "Crime" 24 | }, 25 | { 26 | "id": 99, 27 | "name": "Documentary" 28 | }, 29 | { 30 | "id": 18, 31 | "name": "Drama" 32 | }, 33 | { 34 | "id": 10751, 35 | "name": "Family" 36 | }, 37 | { 38 | "id": 14, 39 | "name": "Fantasy" 40 | }, 41 | { 42 | "id": 36, 43 | "name": "History" 44 | }, 45 | { 46 | "id": 27, 47 | "name": "Horror" 48 | }, 49 | { 50 | "id": 10402, 51 | "name": "Music" 52 | }, 53 | { 54 | "id": 9648, 55 | "name": "Mystery" 56 | }, 57 | { 58 | "id": 10749, 59 | "name": "Romance" 60 | }, 61 | { 62 | "id": 878, 63 | "name": "Science Fiction" 64 | }, 65 | { 66 | "id": 10770, 67 | "name": "TV Movie" 68 | }, 69 | { 70 | "id": 53, 71 | "name": "Thriller" 72 | }, 73 | { 74 | "id": 10752, 75 | "name": "War" 76 | }, 77 | { 78 | "id": 37, 79 | "name": "Western" 80 | }, 81 | { 82 | "id": 10759, 83 | "name": "Action & Adventure" 84 | }, 85 | { 86 | "id": 16, 87 | "name": "Animation" 88 | }, 89 | { 90 | "id": 35, 91 | "name": "Comedy" 92 | }, 93 | { 94 | "id": 80, 95 | "name": "Crime" 96 | }, 97 | { 98 | "id": 99, 99 | "name": "Documentary" 100 | }, 101 | { 102 | "id": 18, 103 | "name": "Drama" 104 | }, 105 | { 106 | "id": 10751, 107 | "name": "Family" 108 | }, 109 | { 110 | "id": 10762, 111 | "name": "Kids" 112 | }, 113 | { 114 | "id": 9648, 115 | "name": "Mystery" 116 | }, 117 | { 118 | "id": 10763, 119 | "name": "News" 120 | }, 121 | { 122 | "id": 10764, 123 | "name": "Reality" 124 | }, 125 | { 126 | "id": 10765, 127 | "name": "Sci-Fi & Fantasy" 128 | }, 129 | { 130 | "id": 10766, 131 | "name": "Soap" 132 | }, 133 | { 134 | "id": 10767, 135 | "name": "Talk" 136 | }, 137 | { 138 | "id": 10768, 139 | "name": "War & Politics" 140 | }, 141 | { 142 | "id": 37, 143 | "name": "Western" 144 | } 145 | ] 146 | } 147 | 148 | export const genreSlice = createSlice({ 149 | name: "genre", 150 | initialState, 151 | reducers: {}, 152 | }) 153 | 154 | export default genreSlice.reducer -------------------------------------------------------------------------------- /client/src/components/artist/ArtistDetails.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import ArtistCredits from "./ArtistCredits"; 3 | import ArtistPhotos from "./ArtistPhotos"; 4 | import ArtistSummary from "./ArtistSummary"; 5 | import CardListing from "../general/CardListing"; 6 | 7 | const ArtistDetails = ({ data }) => { 8 | const [knownFor, setKnownFor] = useState(true); 9 | const [credits, setCredits] = useState(false); 10 | const [photos, setPhotos] = useState(false); 11 | 12 | const handleKnownFor = () => { 13 | setKnownFor(true); 14 | setCredits(false); 15 | setPhotos(false); 16 | }; 17 | const handleCredits = () => { 18 | setCredits(true); 19 | setKnownFor(false); 20 | setPhotos(false); 21 | }; 22 | const handlePhotos = () => { 23 | setPhotos(true); 24 | setKnownFor(false); 25 | setCredits(false); 26 | }; 27 | return ( 28 |
29 |
30 | 31 |
32 | 33 | {/* Tab Header */} 34 |
38 | 47 | 56 | 65 |
66 | 67 |
68 | {/* Known For */} 69 |
70 | 75 |
76 | 77 | {/* Credits */} 78 |
79 | 80 |
81 | 82 | {/* Photos */} 83 |
84 | 85 |
86 |
87 |
88 | ); 89 | }; 90 | export default ArtistDetails; 91 | -------------------------------------------------------------------------------- /client/src/redux/userApi.js: -------------------------------------------------------------------------------- 1 | import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; 2 | 3 | export const userApi = createApi({ 4 | reducerPath: "userApi", 5 | baseQuery: fetchBaseQuery({ 6 | baseUrl: process.env.REACT_APP_BACKEND_URL || "", 7 | }), 8 | tagTypes: ["collection"], 9 | endpoints: (builder) => ({ 10 | // register user 11 | register: builder.mutation({ 12 | query(user) { 13 | return { 14 | url: "authorization/register", 15 | method: "POST", 16 | body: user, 17 | }; 18 | }, 19 | }), 20 | // login user 21 | login: builder.mutation({ 22 | query(user) { 23 | return { 24 | url: "authorization/login", 25 | method: "POST", 26 | body: user, 27 | }; 28 | }, 29 | }), 30 | // Get collection 31 | getCollection: builder.query({ 32 | query: (token) => { 33 | return { 34 | url: "user/collection", 35 | method: "GET", 36 | headers: { Authorization: "Bearer " + token }, 37 | }; 38 | }, 39 | providesTags: ["collection"], 40 | }), 41 | // Add collection 42 | addLike: builder.mutation({ 43 | query(args) { 44 | const { data, token } = args; 45 | return { 46 | url: "user/collection", 47 | method: "POST", 48 | body: data, 49 | headers: { Authorization: "Bearer " + token }, 50 | }; 51 | }, 52 | invalidatesTags: ["collection"], 53 | }), 54 | addWatchLater: builder.mutation({ 55 | query(args) { 56 | const { data, token } = args; 57 | return { 58 | url: "user/collection", 59 | method: "POST", 60 | body: data, 61 | headers: { Authorization: "Bearer " + token }, 62 | }; 63 | }, 64 | invalidatesTags: ["collection"], 65 | }), 66 | // Delete collection 67 | removeLike: builder.mutation({ 68 | query(args) { 69 | const { id, token } = args; 70 | return { 71 | url: `user/collection/${id}`, 72 | method: "DELETE", 73 | headers: { Authorization: "Bearer " + token }, 74 | }; 75 | }, 76 | invalidatesTags: ["collection"], 77 | }), 78 | // Delete collection 79 | removeWatchLater: builder.mutation({ 80 | query(args) { 81 | const { id, token } = args; 82 | return { 83 | url: `user/collection/${id}`, 84 | method: "DELETE", 85 | headers: { Authorization: "Bearer " + token }, 86 | }; 87 | }, 88 | invalidatesTags: ["collection"], 89 | }), 90 | }), 91 | }); 92 | 93 | export const { 94 | useRegisterMutation, 95 | useLoginMutation, 96 | useGetCollectionQuery, 97 | useAddLikeMutation, 98 | useAddWatchLaterMutation, 99 | useRemoveLikeMutation, 100 | useRemoveWatchLaterMutation, 101 | } = userApi; 102 | -------------------------------------------------------------------------------- /client/src/components/user/UserTabs.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { useGetCollectionQuery } from "../../redux/userApi"; 3 | import UserMovies from "./UserMovies"; 4 | import UserPerson from "./UserPerson"; 5 | import UserShows from "./UserShows"; 6 | 7 | const UserTabs = ({ token }) => { 8 | const [movies, setMovies] = useState(true); 9 | const [shows, setShows] = useState(false); 10 | const [person, setPerson] = useState(false); 11 | const { data } = useGetCollectionQuery(token); 12 | 13 | const handleMovies = () => { 14 | setMovies(true); 15 | setShows(false); 16 | setPerson(false); 17 | }; 18 | const handleShows = () => { 19 | setShows(true); 20 | setMovies(false); 21 | setPerson(false); 22 | }; 23 | const handlePerson = () => { 24 | setPerson(true); 25 | setMovies(false); 26 | setShows(false); 27 | }; 28 | return ( 29 |
30 | {/* Tab Header */} 31 |
35 | 44 | 53 | 62 |
63 | 64 |
65 | {/* Known For */} 66 |
67 | item.mediaType === "movie")} 69 | /> 70 |
71 | 72 | {/* Credits */} 73 |
74 | item.mediaType === "show" || item.mediaType === "tv" 77 | )} 78 | /> 79 |
80 | 81 | {/* Photos */} 82 |
83 | item.mediaType === "person" 86 | )} 87 | /> 88 |
89 |
90 |
91 | ); 92 | }; 93 | export default UserTabs; 94 | -------------------------------------------------------------------------------- /client/cypress/e2e/information/actions.cy.js: -------------------------------------------------------------------------------- 1 | describe("Play the trailer", () => { 2 | // Play the trailer 3 | it("Play the trailer", () => { 4 | cy.visit("/movies/157336"); 5 | cy.getCypress("hero-mobile-play-button").should("exist"); 6 | cy.getCypress("hero-mobile-play-button").should("be.visible"); 7 | cy.getCypress("hero-mobile-play-button").click(); 8 | cy.getCypress("hero-video-player").should("exist"); 9 | cy.getCypress("hero-video-player").should("be.visible"); 10 | cy.getCypress("hero-video-close-button").should("exist"); 11 | cy.getCypress("hero-video-close-button").should("be.visible"); 12 | cy.getCypress("hero-video-close-button").click(); 13 | }); 14 | 15 | // Check the videos tab 16 | it("Check the videos tab", () => { 17 | cy.visit("/movies/157336"); 18 | cy.getCypress("details-tab-videos").should("exist"); 19 | cy.getCypress("details-tab-videos").should("be.visible"); 20 | cy.getCypress("details-tab-videos").click(); 21 | cy.getCypress("details-videos-container").should("exist"); 22 | cy.getCypress("details-videos-container").should("be.visible"); 23 | cy.getCypress("details-videos-single").should("exist"); 24 | cy.getCypress("details-videos-single").should("be.visible"); 25 | cy.getCypress("details-videos-single").first().click(); 26 | }); 27 | 28 | // Check the photos tab 29 | it("Check the photos tab", () => { 30 | cy.visit("/movies/157336"); 31 | cy.getCypress("details-tab-photos").should("exist"); 32 | cy.getCypress("details-tab-photos").should("be.visible"); 33 | cy.getCypress("details-tab-photos").click(); 34 | cy.getCypress("details-photos-headline").should("exist"); 35 | cy.getCypress("details-photos-headline").should("be.visible"); 36 | cy.getCypress("details-photos-headline").should("have.text", "Backdrops"); 37 | cy.getCypress("details-photos-container").should("exist"); 38 | cy.getCypress("details-photos-container").should("be.visible"); 39 | cy.getCypress("details-photos-single").should("exist"); 40 | cy.getCypress("details-photos-single").should("be.visible"); 41 | cy.getCypress("details-photos-single").first().click(); 42 | cy.getCypress("details-photos-close").should("exist"); 43 | cy.getCypress("details-photos-close").should("be.visible"); 44 | cy.getCypress("details-photos-close").click(); 45 | }); 46 | 47 | // Check the episodes tab 48 | it("Check the episodes tab", () => { 49 | cy.visit("/shows/1396"); 50 | cy.getCypress("details-tab-episodes").should("exist"); 51 | cy.getCypress("details-tab-episodes").should("be.visible"); 52 | cy.getCypress("details-tab-episodes").click(); 53 | cy.getCypress("details-episodes-headline").should("exist"); 54 | cy.getCypress("details-episodes-headline").should("be.visible"); 55 | cy.getCypress("details-episodes-headline").should("have.text", "Episodes"); 56 | cy.getCypress("details-episodes-container").should("exist"); 57 | cy.getCypress("details-episodes-container").should("be.visible"); 58 | cy.getCypress("details-episodes-single").should("exist"); 59 | cy.getCypress("details-episodes-single").should("be.visible"); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /client/src/components/details/DetailsVideos.js: -------------------------------------------------------------------------------- 1 | import { useRef, useState } from "react"; 2 | import ReactPlayer from "react-player/youtube"; 3 | import { FaRegPlayCircle } from "@react-icons/all-files/fa/FaRegPlayCircle"; 4 | 5 | const DetailsVideos = ({ data }) => { 6 | const [videoFilter, setVideoFilter] = useState(""); 7 | const videoRef = useRef(); 8 | 9 | const handleFilter = (e) => { 10 | setVideoFilter(e.target.value); 11 | }; 12 | return ( 13 |
14 | {/* Filter */} 15 | 41 | 42 | {/* Container */} 43 |
47 | {data && 48 | data 49 | .filter((item) => item.type.includes(videoFilter)) 50 | .map((item) => ( 51 |
55 |
59 | 68 | } 69 | light={true} 70 | onEnded={() => videoRef.current.showPreview()} 71 | /> 72 |
73 |
74 |

75 | {item.name} 76 |

77 |

{item.type}

78 |
79 |
80 | ))} 81 |
82 |
83 | ); 84 | }; 85 | export default DetailsVideos; 86 | -------------------------------------------------------------------------------- /client/src/service-worker.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-globals */ 2 | 3 | // This service worker can be customized! 4 | // See https://developers.google.com/web/tools/workbox/modules 5 | // for the list of available Workbox modules, or add any other 6 | // code you'd like. 7 | // You can also remove this file if you'd prefer not to use a 8 | // service worker, and the Workbox build step will be skipped. 9 | 10 | import { clientsClaim } from 'workbox-core'; 11 | import { ExpirationPlugin } from 'workbox-expiration'; 12 | import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching'; 13 | import { registerRoute } from 'workbox-routing'; 14 | import { StaleWhileRevalidate } from 'workbox-strategies'; 15 | 16 | // Offine fallback 17 | import {setDefaultHandler} from 'workbox-routing'; 18 | import {NetworkOnly} from 'workbox-strategies'; 19 | import {offlineFallback} from 'workbox-recipes'; 20 | 21 | clientsClaim(); 22 | 23 | // Precache all of the assets generated by your build process. 24 | // Their URLs are injected into the manifest variable below. 25 | // This variable must be present somewhere in your service worker file, 26 | // even if you decide not to use precaching. See https://cra.link/PWA 27 | precacheAndRoute(self.__WB_MANIFEST); 28 | 29 | // Set up App Shell-style routing, so that all navigation requests 30 | // are fulfilled with your index.html shell. Learn more at 31 | // https://developers.google.com/web/fundamentals/architecture/app-shell 32 | const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$'); 33 | registerRoute( 34 | // Return false to exempt requests from being fulfilled by index.html. 35 | ({ request, url }) => { 36 | // If this isn't a navigation, skip. 37 | if (request.mode !== 'navigate') { 38 | return false; 39 | } // If this is a URL that starts with /_, skip. 40 | 41 | if (url.pathname.startsWith('/_')) { 42 | return false; 43 | } // If this looks like a URL for a resource, because it contains // a file extension, skip. 44 | 45 | if (url.pathname.match(fileExtensionRegexp)) { 46 | return false; 47 | } // Return true to signal that we want to use the handler. 48 | 49 | return true; 50 | }, 51 | createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html') 52 | ); 53 | 54 | // An example runtime caching route for requests that aren't handled by the 55 | // precache, in this case same-origin .png requests like those from in public/ 56 | registerRoute( 57 | // Add in any other file extensions or routing criteria as needed. 58 | ({ url }) => url.origin === self.location.origin && url.pathname.endsWith('.png'), // Customize this strategy as needed, e.g., by changing to CacheFirst. 59 | new StaleWhileRevalidate({ 60 | cacheName: 'images', 61 | plugins: [ 62 | // Ensure that once this runtime cache reaches a maximum size the 63 | // least-recently used images are removed. 64 | new ExpirationPlugin({ maxEntries: 50 }), 65 | ], 66 | }) 67 | ); 68 | 69 | // This allows the web app to trigger skipWaiting via 70 | // registration.waiting.postMessage({type: 'SKIP_WAITING'}) 71 | self.addEventListener('message', (event) => { 72 | if (event.data && event.data.type === 'SKIP_WAITING') { 73 | self.skipWaiting(); 74 | } 75 | }); 76 | 77 | // Any other custom service worker logic can go here. 78 | 79 | // Offline fallback 80 | setDefaultHandler(new NetworkOnly()); 81 | offlineFallback(); -------------------------------------------------------------------------------- /client/src/components/general/Navbar.js: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | 3 | import { BiHomeSmile } from "@react-icons/all-files/bi/BiHomeSmile"; 4 | import { BiSearch } from "@react-icons/all-files/bi/BiSearch"; 5 | import { BiCameraMovie } from "@react-icons/all-files/bi/BiCameraMovie"; 6 | import { BiTv } from "@react-icons/all-files/bi/BiTv"; 7 | import { BiUserCircle } from "@react-icons/all-files/bi/BiUserCircle"; 8 | 9 | import { setSearch } from "../../redux/layoutSlice"; 10 | import { useDispatch, useSelector } from "react-redux"; 11 | 12 | const Navbar = () => { 13 | const dispatch = useDispatch(); 14 | const isSearch = useSelector((state) => state.layout.isSearch); 15 | 16 | return ( 17 |
21 |
22 | {/* Home */} 23 | 34 | {/* Search */} 35 | 47 | {/* Movies */} 48 | 59 | {/* Shows */} 60 | 71 | {/* Account */} 72 | 83 |
84 |
85 | ); 86 | }; 87 | export default Navbar; 88 | -------------------------------------------------------------------------------- /client/src/components/rating/HeroRating.js: -------------------------------------------------------------------------------- 1 | const HeroRating = ({ data }) => { 2 | return ( 3 | <> 4 | {data.vote_average > 0 ? ( 5 |
9 | 19 | 20 | 2 ? true : false 28 | } 29 | /> 30 | 31 | 5 ? true : false 39 | } 40 | /> 41 | 42 | 7 ? true : false 50 | } 51 | /> 52 | 53 | 9 && data?.vote_average === 0 ? true : false 61 | } 62 | /> 63 |
64 | ) : ( 65 |
69 | 77 | 78 | 84 | 85 | 91 | 92 | 98 | 99 | 105 | 106 | 112 |
113 | )} 114 | 115 | ); 116 | }; 117 | export default HeroRating; 118 | -------------------------------------------------------------------------------- /client/src/assets/account.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/loaders/partials/UserDetails.js: -------------------------------------------------------------------------------- 1 | const UserDetails = () => { 2 | return ( 3 |
4 | {/* Mobile & Tablet Heading */} 5 |
6 | 7 |
8 | {/* Image */} 9 | {/* w-[45%] lg:w-full lg:max-h-[556px] lg:max-w-[370px] */} 10 |
11 | {/* Texts */} 12 |
13 | {/* Desktop Heading */} 14 |
15 |

16 | Lorem ipsum, dolor sit amet consectetur adipisicing elit. Suscipit 17 | ut, consequatur ipsam perspiciatis rerum molestiae incidunt 18 | repudiandae explicabo, culpa molestias, amet laboriosam nam 19 | temporibus. Similique odio perspiciatis quos eos pariatur? 20 |

21 |

22 | Lorem ipsum, dolor sit amet consectetur adipisicing elit. Suscipit 23 | ut, consequatur ipsam perspiciatis rerum molestiae incidunt 24 | repudiandae explicabo, culpa molestias, amet laboriosam nam 25 | temporibus. Similique odio perspiciatis quos eos pariatur? 26 |

27 |

28 | Lorem ipsum, dolor sit amet consectetur adipisicing elit. Suscipit 29 | ut, consequatur ipsam perspiciatis rerum molestiae incidunt 30 | repudiandae explicabo, culpa molestias, amet laboriosam nam 31 | temporibus. Similique odio perspiciatis quos eos pariatur? 32 |

33 | 34 |
35 |

36 | Known For 37 |

38 |

39 | Acting 40 |

41 |
42 |
43 |

44 | Born 45 |

46 |

47 | July 13, 1999 (age 23) 48 |

49 |
50 |
51 |

52 | Place of Birth 53 |

54 |

55 | India 56 |

57 |
58 |
59 |

60 | ⚫ 61 |

62 |

63 | ⚫ 64 |

65 |

66 | ⚫ 67 |

68 |
69 |
70 |
71 |
72 | ); 73 | }; 74 | export default UserDetails; 75 | -------------------------------------------------------------------------------- /client/src/components/general/Search.js: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect, useState } from "react"; 2 | import { useLocation, useSearchParams } from "react-router-dom"; 3 | import { useSelector, useDispatch } from "react-redux"; 4 | import { setSearch } from "../../redux/layoutSlice"; 5 | import { useNavigate } from "react-router-dom"; 6 | import { FaTimes } from "@react-icons/all-files/fa/FaTimes"; 7 | 8 | const Search = () => { 9 | const { isSearch } = useSelector((state) => state.layout); 10 | const searchBarRef = useRef(); 11 | const searchInputRef = useRef(); 12 | const dispatch = useDispatch(); 13 | const navigate = useNavigate(); 14 | const location = useLocation(); 15 | const [searchParam] = useSearchParams(); 16 | const [lastPage, setLastPage] = useState(""); 17 | 18 | // Get last path excluding search page 19 | useEffect(() => { 20 | if (location && !location.pathname.includes("/search")) { 21 | setLastPage(location.pathname); 22 | dispatch(setSearch(false)); 23 | } 24 | }, [location, dispatch]); 25 | 26 | // Handle search bar open & close 27 | useEffect(() => { 28 | const handleClickOutside = (e) => { 29 | if ( 30 | searchBarRef.current && 31 | !searchBarRef.current.contains(e.target) && 32 | !location.pathname.includes("/search") && 33 | e.target.id !== "searchButton" && 34 | e.target.id !== "searchIcon" 35 | ) { 36 | dispatch(setSearch(false)); 37 | } 38 | }; 39 | 40 | document.addEventListener("click", handleClickOutside, true); 41 | return () => { 42 | document.removeEventListener("click", handleClickOutside, true); 43 | }; 44 | }, [dispatch, location]); 45 | 46 | // Handle Search Params 47 | const handleSearchParam = (e) => { 48 | const term = e.target.value; 49 | if (term) { 50 | navigate({ 51 | pathname: "/search", 52 | search: `?q=${term}`, 53 | }); 54 | } 55 | // Go back to previous page 56 | if (!term) { 57 | navigate(lastPage); 58 | setSearch(false); 59 | } 60 | }; 61 | 62 | // Handle Search Clear 63 | const handleSearchClear = () => { 64 | navigate(lastPage); 65 | setSearch(false); 66 | }; 67 | 68 | return ( 69 | <> 70 | {isSearch && ( 71 |
75 | {/* Search Field */} 76 |
80 |
81 | 91 |
92 |
93 | 94 | {/* Cancel Button */} 95 | {searchParam.get("q") && ( 96 | 102 | )} 103 |
104 | )} 105 | 106 | ); 107 | }; 108 | export default Search; 109 | -------------------------------------------------------------------------------- /client/src/loaders/partials/Cards.js: -------------------------------------------------------------------------------- 1 | const Cards = () => { 2 | return ( 3 |
4 | {/* Heading */} 5 |
6 | {/* Cards */} 7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | ); 39 | }; 40 | export default Cards; 41 | -------------------------------------------------------------------------------- /client/src/components/artist/ArtistCredits.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import ActingDepartment from "./department/ActingDepartment"; 3 | import DirectingDepartment from "./department/DirectingDepartment"; 4 | import ProductionDepartment from "./department/ProductionDepartment"; 5 | import WritingDepartment from "./department/WritingDepartment"; 6 | 7 | const ArtistCredits = ({ data }) => { 8 | const [departmentFilter, setDepartmentFilter] = useState(""); 9 | const [mediaFilter, setMediaFilter] = useState(""); 10 | const handleDepartmentFilter = (e) => { 11 | setDepartmentFilter(e.target.value); 12 | }; 13 | 14 | const handleMediaFilter = (e) => { 15 | setMediaFilter(e.target.value); 16 | }; 17 | return ( 18 |
19 | {/* Filter */} 20 |
24 |
25 | 29 | Department 30 | 31 | 51 |
52 | 53 |
54 | 58 | Media 59 | 60 | 74 |
75 |
76 | 77 | {/* Container */} 78 | {/* Acting */} 79 | {(departmentFilter === "" || departmentFilter === "acting") && ( 80 | 81 | )} 82 | 83 | {/* Directing */} 84 | {(departmentFilter === "" || departmentFilter === "directing") && ( 85 | 86 | )} 87 | 88 | {/* Production */} 89 | {(departmentFilter === "" || departmentFilter === "production") && ( 90 | 91 | )} 92 | 93 | {/* Writing */} 94 | {(departmentFilter === "" || departmentFilter === "writing") && ( 95 | 96 | )} 97 |
98 | ); 99 | }; 100 | export default ArtistCredits; 101 | -------------------------------------------------------------------------------- /client/src/assets/EmptyCard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/components/general/Footer.js: -------------------------------------------------------------------------------- 1 | import { FaGithub } from "@react-icons/all-files/fa/FaGithub"; 2 | import { FaLinkedinIn } from "@react-icons/all-files/fa/FaLinkedinIn"; 3 | import { FaYoutube } from "@react-icons/all-files/fa/FaYoutube"; 4 | import { FaEnvelope } from "@react-icons/all-files/fa/FaEnvelope"; 5 | import { FaGlobe } from "@react-icons/all-files/fa/FaGlobe"; 6 | 7 | const Footer = () => { 8 | return ( 9 | 113 | ); 114 | }; 115 | export default Footer; 116 | -------------------------------------------------------------------------------- /client/src/components/artist/ArtistPhotos.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import LoadingCard from "../../assets/LoadingCard.svg"; 3 | import EmptyCard from "../../assets/EmptyCard.svg"; 4 | import { useSelector } from "react-redux"; 5 | import LazyImage from "../../loaders/LazyImage"; 6 | 7 | import { FaChevronLeft } from "@react-icons/all-files/fa/FaChevronLeft"; 8 | import { FaChevronRight } from "@react-icons/all-files/fa/FaChevronRight"; 9 | import { FaTimes } from "@react-icons/all-files/fa/FaTimes"; 10 | 11 | const ArtistPhotos = ({ data }) => { 12 | const poster = useSelector((state) => state.layout.poster); 13 | const [slideNumber, setSlideNumber] = useState(0); 14 | const [touchPosition, setTouchPosition] = useState(null); 15 | 16 | const handleLeft = () => { 17 | slideNumber === 0 18 | ? setSlideNumber(data.length - 1) 19 | : setSlideNumber(slideNumber - 1); 20 | }; 21 | const handleRight = () => { 22 | slideNumber + 1 === data.length 23 | ? setSlideNumber(0) 24 | : setSlideNumber(slideNumber + 1); 25 | }; 26 | 27 | const handleTouchStart = (e) => { 28 | const touchPosition = e.touches[0].clientX; 29 | setTouchPosition(touchPosition); 30 | }; 31 | 32 | const handleTouchMove = (e) => { 33 | if (touchPosition === null) return; 34 | 35 | const touchMove = e.touches[0].clientX; 36 | const difference = touchPosition - touchMove; 37 | 38 | if (difference > 5) { 39 | handleRight(); 40 | } 41 | 42 | if (difference < -5) { 43 | handleLeft(); 44 | } 45 | 46 | setTouchPosition(null); 47 | }; 48 | 49 | const handleModal = (index) => { 50 | setSlideNumber(index); 51 | }; 52 | return ( 53 |
54 | {/* Modal */} 55 | 56 | 95 | 96 | {/* Posters */} 97 |

101 | Photos 102 |

103 |
107 | {data && 108 | data.map((item, index) => ( 109 | 122 | ))} 123 |
124 |
125 | ); 126 | }; 127 | export default ArtistPhotos; 128 | -------------------------------------------------------------------------------- /client/cypress/e2e/artistInformation/artistInformation.cy.js: -------------------------------------------------------------------------------- 1 | import { checkFooter } from "../../helpers/checkFooter"; 2 | import { checkNavigation } from "../../helpers/checkNavigation"; 3 | 4 | describe("Check artist information page", () => { 5 | // Visit the artist page 6 | beforeEach(() => { 7 | cy.visit("/person/380"); 8 | }); 9 | 10 | // Check artist content & information 11 | it("Check artist content & information", () => { 12 | cy.getCypress("artist-image").should("exist"); 13 | cy.getCypress("artist-image").should("be.visible"); 14 | cy.getCypress("artist-name").should("exist"); 15 | cy.getCypress("artist-name").should("be.visible"); 16 | cy.getCypress("artist-name").should("have.text", "Robert De Niro"); 17 | cy.getCypress("artist-biography").should("exist"); 18 | cy.getCypress("artist-biography").should("be.visible"); 19 | cy.getCypress("artist-biography").should( 20 | "contain.text", 21 | "Robert Anthony De Niro" 22 | ); 23 | cy.getCypress("artist-known-for").should("exist"); 24 | cy.getCypress("artist-known-for").should("be.visible"); 25 | cy.getCypress("artist-known-for").should("have.text", "Acting"); 26 | }); 27 | 28 | // Check artist tabs 29 | it("Check artist tabs", () => { 30 | cy.getCypress("artist-tabs-container").should("exist"); 31 | cy.getCypress("artist-tabs-container").should("be.visible"); 32 | cy.getCypress("artist-known-for-tab").should("exist"); 33 | cy.getCypress("artist-known-for-tab").should("be.visible"); 34 | cy.getCypress("artist-known-for-tab").should("have.text", "KNOWN FOR"); 35 | cy.getCypress("artist-known-for-tab").should("have.class", "tab-active"); 36 | cy.getCypress("artist-credits-tab").should("exist"); 37 | cy.getCypress("artist-credits-tab").should("be.visible"); 38 | cy.getCypress("artist-credits-tab").should("have.text", "CREDITS"); 39 | cy.getCypress("artist-credits-tab").click(); 40 | cy.getCypress("artist-credits-tab").should("have.class", "tab-active"); 41 | cy.getCypress("artist-photos-tab").should("exist"); 42 | cy.getCypress("artist-photos-tab").should("be.visible"); 43 | cy.getCypress("artist-photos-tab").should("have.text", "PHOTOS"); 44 | cy.getCypress("artist-photos-tab").click(); 45 | cy.getCypress("artist-photos-tab").should("have.class", "tab-active"); 46 | }); 47 | 48 | // Check artist known for listing 49 | it("Check artist known for listing", () => { 50 | cy.getCypress("artist-tabs-container").should("exist"); 51 | cy.getCypress("artist-tabs-container").should("be.visible"); 52 | cy.getCypress("artist-known-for-tab").should("exist"); 53 | cy.getCypress("artist-known-for-tab").should("be.visible"); 54 | cy.getCypress("artist-known-for-tab").click(); 55 | cy.getCypress("card-listing-item").should("exist"); 56 | cy.getCypress("card-listing-item").should("be.visible"); 57 | cy.getCypress("card-listing-item").first().click(); 58 | cy.url().should("match", /\/movies\/|\/shows\//); 59 | }); 60 | 61 | // Check artist credits listing 62 | it("Check artist credits listing", () => { 63 | cy.getCypress("artist-tabs-container").should("exist"); 64 | cy.getCypress("artist-tabs-container").should("be.visible"); 65 | cy.getCypress("artist-credits-tab").should("exist"); 66 | cy.getCypress("artist-credits-tab").should("be.visible"); 67 | cy.getCypress("artist-credits-tab").click(); 68 | cy.getCypress("artist-credit-department").should("exist"); 69 | cy.getCypress("artist-credit-department").should("be.visible"); 70 | cy.getCypress("artist-credit-department").should("have.text", "Department"); 71 | cy.getCypress("artist-credit-media").should("exist"); 72 | cy.getCypress("artist-credit-media").should("be.visible"); 73 | cy.getCypress("artist-credit-media").should("have.text", "Media"); 74 | }); 75 | 76 | // Check artist photos listing 77 | it("Check artist photos listing", () => { 78 | cy.getCypress("artist-tabs-container").should("exist"); 79 | cy.getCypress("artist-tabs-container").should("be.visible"); 80 | cy.getCypress("artist-photos-tab").should("exist"); 81 | cy.getCypress("artist-photos-tab").should("be.visible"); 82 | cy.getCypress("artist-photos-tab").click(); 83 | cy.getCypress("artist-photos-headline").should("exist"); 84 | cy.getCypress("artist-photos-headline").should("be.visible"); 85 | cy.getCypress("artist-photos-headline").should("have.text", "Photos"); 86 | cy.getCypress("artist-photos-container").should("exist"); 87 | cy.getCypress("artist-photos-container").should("be.visible"); 88 | cy.getCypress("artist-photos-single").should("exist"); 89 | cy.getCypress("artist-photos-single").should("be.visible"); 90 | cy.getCypress("artist-photos-single").first().click(); 91 | cy.getCypress("artist-photos-close").should("exist"); 92 | cy.getCypress("artist-photos-close").should("be.visible"); 93 | cy.getCypress("artist-photos-close").click(); 94 | }); 95 | 96 | // Check the footer 97 | checkFooter(); 98 | 99 | // Check the navigation 100 | checkNavigation(); 101 | }); 102 | -------------------------------------------------------------------------------- /client/cypress/e2e/information/information.cy.js: -------------------------------------------------------------------------------- 1 | import { checkFooter } from "../../helpers/checkFooter"; 2 | import { checkHero } from "../../helpers/checkHero"; 3 | import { checkNavigation } from "../../helpers/checkNavigation"; 4 | 5 | describe("Check information page", () => { 6 | // Visit the home page 7 | beforeEach(() => { 8 | cy.visit("/"); 9 | cy.getCypress("navigation-search").click(); 10 | cy.getCypress("search-field-input").type("The Shawshank Redemption"); 11 | cy.getCypress("card-listing-item").first().click(); 12 | }); 13 | 14 | // Get the content of the hero 15 | it("Check the content of the hero", () => { 16 | checkHero(); 17 | }); 18 | 19 | // Check the details tab 20 | it("Check the details tab", () => { 21 | cy.getCypress("details-tab-container").should("exist"); 22 | cy.getCypress("details-tab-container").should("be.visible"); 23 | cy.getCypress("details-tab-overview").should("exist"); 24 | cy.getCypress("details-tab-overview").should("be.visible"); 25 | cy.getCypress("details-tab-overview").should("have.class", "tab-active"); 26 | cy.getCypress("details-tab-videos").should("exist"); 27 | cy.getCypress("details-tab-videos").should("be.visible"); 28 | cy.getCypress("details-tab-videos").click(); 29 | cy.getCypress("details-tab-videos").should("have.class", "tab-active"); 30 | cy.getCypress("details-tab-photos").should("exist"); 31 | cy.getCypress("details-tab-photos").should("be.visible"); 32 | cy.getCypress("details-tab-photos").click(); 33 | cy.getCypress("details-tab-photos").should("have.class", "tab-active"); 34 | }); 35 | 36 | // Check the information summary 37 | it("Check the information summary", () => { 38 | cy.getCypress("information-headline").should("exist"); 39 | cy.getCypress("information-headline").should("be.visible"); 40 | cy.getCypress("information-headline").should("have.text", "Summary"); 41 | cy.getCypress("information-overview").should("exist"); 42 | cy.getCypress("information-overview").should("be.visible"); 43 | }); 44 | 45 | // Check the cast carousel 46 | it("Check the cast carousel", () => { 47 | cy.getCypress("cast-carousel-heading").should("exist"); 48 | cy.getCypress("cast-carousel-heading").should("be.visible"); 49 | cy.getCypress("cast-carousel-heading").should("have.text", "Cast"); 50 | cy.getCypress("cast-carousel-items-container").should("exist"); 51 | cy.getCypress("cast-carousel-items-container").should("be.visible"); 52 | cy.getCypress("cast-carousel-item-link").should("exist"); 53 | cy.getCypress("cast-carousel-item-link").should("be.visible"); 54 | cy.getCypress("cast-carousel-item-link").first().click(); 55 | cy.url().should("include", "/person"); 56 | }); 57 | 58 | // Check the similar carousel 59 | it("Check the similar carousel", () => { 60 | cy.getCypress("carousel-heading-text").should("exist"); 61 | cy.getCypress("carousel-heading-text").should("be.visible"); 62 | cy.getCypress("carousel-heading-text").should( 63 | "have.text", 64 | "More Like This" 65 | ); 66 | cy.getCypress("carousel-items-container").should("exist"); 67 | cy.getCypress("carousel-items-container").should("be.visible"); 68 | cy.getCypress("carousel-item-link").should("exist"); 69 | cy.getCypress("carousel-item-link").should("be.visible"); 70 | cy.getCypress("carousel-item-link").first().click(); 71 | cy.url().should("include", "/movies"); 72 | }); 73 | 74 | // Check the footer 75 | checkFooter(); 76 | 77 | // Check the navigation 78 | checkNavigation(); 79 | }); 80 | 81 | // Check details tab for show 82 | describe("Check details for show", () => { 83 | // Visit the home page 84 | beforeEach(() => { 85 | cy.visit("/"); 86 | cy.getCypress("navigation-search").click(); 87 | cy.getCypress("search-field-input").type("Breaking Bad"); 88 | cy.getCypress("card-listing-item").first().click(); 89 | }); 90 | 91 | // Check the details tab 92 | it("Check the details tab", () => { 93 | cy.getCypress("details-tab-container").should("exist"); 94 | cy.getCypress("details-tab-container").should("be.visible"); 95 | cy.getCypress("details-tab-overview").should("exist"); 96 | cy.getCypress("details-tab-overview").should("be.visible"); 97 | cy.getCypress("details-tab-overview").should("have.class", "tab-active"); 98 | cy.getCypress("details-tab-episodes").should("exist"); 99 | cy.getCypress("details-tab-episodes").should("be.visible"); 100 | cy.getCypress("details-tab-episodes").click(); 101 | cy.getCypress("details-tab-episodes").should("have.class", "tab-active"); 102 | cy.getCypress("details-tab-videos").should("exist"); 103 | cy.getCypress("details-tab-videos").should("be.visible"); 104 | cy.getCypress("details-tab-videos").click(); 105 | cy.getCypress("details-tab-videos").should("have.class", "tab-active"); 106 | cy.getCypress("details-tab-photos").should("exist"); 107 | cy.getCypress("details-tab-photos").should("be.visible"); 108 | cy.getCypress("details-tab-photos").click(); 109 | cy.getCypress("details-tab-photos").should("have.class", "tab-active"); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /client/src/redux/tmdbApi.js: -------------------------------------------------------------------------------- 1 | import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; 2 | 3 | export const tmdbApi = createApi({ 4 | reducerPath: "tmdbApi", 5 | baseQuery: fetchBaseQuery({ 6 | baseUrl: `${process.env.REACT_APP_BACKEND_URL || ""}api`, 7 | }), 8 | endpoints: (builder) => ({ 9 | getTrending: builder.query({ 10 | query: (type) => `/trending/${type}`, 11 | }), 12 | 13 | getSingleTrending: builder.query({ 14 | async queryFn(type, _queryApi, _extraOptions, fetchWithBQ) { 15 | const trendingList = await fetchWithBQ(`/trending/${type}`); 16 | if (trendingList.error) return { error: trendingList.error }; 17 | const singleData = await trendingList.data.results.filter( 18 | (single) => single.media_type !== "person" 19 | )[Math.floor(Math.random() * 10)]; 20 | if (singleData.media_type === "movie") { 21 | const result = await fetchWithBQ(`/trending/movie/${singleData.id}`); 22 | return result.data 23 | ? { data: { ...result.data, mediaType: "movie" } } 24 | : { error: result.error }; 25 | } 26 | if (singleData.media_type === "tv") { 27 | const result = await fetchWithBQ(`/trending/show/${singleData.id}`); 28 | return result.data 29 | ? { data: { ...result.data, mediaType: "show" } } 30 | : { error: result.error }; 31 | } 32 | }, 33 | }), 34 | 35 | getMovieList: builder.query({ 36 | query: (type) => `/list/movie/${type}`, 37 | }), 38 | 39 | getShowList: builder.query({ 40 | query: (type) => `/list/show/${type}`, 41 | }), 42 | 43 | getMovieTrendingPage: builder.query({ 44 | query: (page = 1) => `/trending/movie/page/${page}`, 45 | }), 46 | 47 | getMovieNowPlayingPage: builder.query({ 48 | query: (page = 1) => `/movie/nowplaying/${page}`, 49 | }), 50 | 51 | getMoviePopularPage: builder.query({ 52 | query: (page = 1) => `/movie/popular/${page}`, 53 | }), 54 | 55 | getMovieTopRatedPage: builder.query({ 56 | query: (page = 1) => `/movie/toprated/${page}`, 57 | }), 58 | 59 | getMovieUpcomingPage: builder.query({ 60 | query: (page = 1) => `/movie/upcoming/${page}`, 61 | }), 62 | 63 | getMovieDetailsPage: builder.query({ 64 | query: (movie_id) => `/details/movie/${movie_id}`, 65 | }), 66 | 67 | getMovieGenrePage: builder.query({ 68 | query: (args) => { 69 | const { page, genre } = args; 70 | return { 71 | url: `/movie/genre/${genre}/${page}`, 72 | params: { page, genre }, 73 | }; 74 | }, 75 | }), 76 | 77 | getShowTrendingPage: builder.query({ 78 | query: (page = 1) => `/trending/show/page/${page}`, 79 | }), 80 | 81 | getShowAiringTodayPage: builder.query({ 82 | query: (page = 1) => `/show/airingtoday/${page}`, 83 | }), 84 | 85 | getShowPopularPage: builder.query({ 86 | query: (page = 1) => `/show/popular/${page}`, 87 | }), 88 | 89 | getShowTopRatedPage: builder.query({ 90 | query: (page = 1) => `/show/toprated/${page}`, 91 | }), 92 | 93 | getShowOnTheAirPage: builder.query({ 94 | query: (page = 1) => `/show/ontheair/${page}`, 95 | }), 96 | 97 | getShowDetailsPage: builder.query({ 98 | query: (show_id) => `/details/show/${show_id}`, 99 | }), 100 | 101 | getShowEpisodes: builder.query({ 102 | query: (args) => { 103 | const { show_id, seasonNumber } = args; 104 | return { 105 | url: `/episodes/show/${show_id}/${seasonNumber}`, 106 | params: { show_id, seasonNumber }, 107 | }; 108 | }, 109 | }), 110 | 111 | getShowGenrePage: builder.query({ 112 | query: (args) => { 113 | const { page, genre } = args; 114 | return { 115 | url: `/show/genre/${genre}/${page}`, 116 | params: { page, genre }, 117 | }; 118 | }, 119 | }), 120 | 121 | getPersonDetailsPage: builder.query({ 122 | query: (person_id) => `/person/${person_id}`, 123 | }), 124 | 125 | getSearchResults: builder.query({ 126 | query: (args) => { 127 | const { query, page } = args; 128 | return { 129 | url: `/search/${query}/${page}`, 130 | params: { query, page }, 131 | }; 132 | }, 133 | }), 134 | }), 135 | }); 136 | 137 | export const { 138 | useGetTrendingQuery, 139 | useGetSingleTrendingQuery, 140 | useGetMovieListQuery, 141 | useGetShowListQuery, 142 | useGetMovieTrendingPageQuery, 143 | useGetMovieNowPlayingPageQuery, 144 | useGetMoviePopularPageQuery, 145 | useGetMovieTopRatedPageQuery, 146 | useGetMovieUpcomingPageQuery, 147 | useGetMovieDetailsPageQuery, 148 | useGetMovieGenrePageQuery, 149 | useGetShowTrendingPageQuery, 150 | useGetShowAiringTodayPageQuery, 151 | useGetShowPopularPageQuery, 152 | useGetShowTopRatedPageQuery, 153 | useGetShowOnTheAirPageQuery, 154 | useGetShowDetailsPageQuery, 155 | useGetShowEpisodesQuery, 156 | useGetShowGenrePageQuery, 157 | useGetPersonDetailsPageQuery, 158 | useGetSearchResultsQuery, 159 | } = tmdbApi; 160 | -------------------------------------------------------------------------------- /client/src/assets/userHero.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/components/artist/ArtistSummary.js: -------------------------------------------------------------------------------- 1 | import { useSelector } from "react-redux"; 2 | import LoadingCard from "../../assets/LoadingCard.svg"; 3 | import EmptyCard from "../../assets/EmptyCard.svg"; 4 | import LazyImage from "../../loaders/LazyImage"; 5 | 6 | import { FaTwitter } from "@react-icons/all-files/fa/FaTwitter"; 7 | import { FaFacebookF } from "@react-icons/all-files/fa/FaFacebookF"; 8 | import { FaInstagram } from "@react-icons/all-files/fa/FaInstagram"; 9 | import { FaImdb } from "@react-icons/all-files/fa/FaImdb"; 10 | import { FaGlobe } from "@react-icons/all-files/fa/FaGlobe"; 11 | 12 | const ArtistSummary = ({ data }) => { 13 | const poster = useSelector((state) => state.layout.poster); 14 | return ( 15 |
16 | {/* Mobile & Tablet View */} 17 |
18 |

19 | {data?.name} 20 |

21 |
25 | 32 |
33 |
34 | 35 | {/* Desktop View */} 36 |
37 |
38 | 45 |
46 | 47 |
48 |

49 | {data?.name} 50 |

51 |

55 | {data?.biography} 56 |

57 | 58 |
59 |
60 |

Known for

61 | 62 | {data?.known_for_department} 63 | 64 |
65 | {data && data.birthday && ( 66 |
67 |

Born

68 | 69 | {new Date(data.birthday).toLocaleDateString("en-us", { 70 | day: "numeric", 71 | year: "numeric", 72 | month: "long", 73 | })}{" "} 74 | (age{" "} 75 | {new Date().getFullYear() - 76 | new Date(data.birthday).getFullYear()} 77 | ) 78 | 79 |
80 | )} 81 |
82 |

Place of Birth

83 | {data?.place_of_birth} 84 |
85 |
86 | 87 |
88 | {data?.external_ids.instagram_id && ( 89 | 94 | 95 | 96 | )} 97 | {data?.external_ids.twitter_id && ( 98 | 103 | 104 | 105 | )} 106 | {data?.external_ids.facebook_id && ( 107 | 112 | 113 | 114 | )} 115 | {data?.external_ids.imdb_id && ( 116 | 121 | 122 | 123 | )} 124 | {data?.homepage && ( 125 | 126 | 127 | 128 | )} 129 |
130 |
131 |
132 |
133 | ); 134 | }; 135 | export default ArtistSummary; 136 | --------------------------------------------------------------------------------