├── 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 |
13 |
14 |
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 |
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 |
20 |
21 |
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 |
17 |
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 | navigate(-1)}
10 | className="btn btn-circle btn-ghost">
11 |
12 |
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 |
28 |
29 |
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 |
23 |
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 |
21 | >
22 | ) : (
23 |
24 | )}
25 |
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 |
29 | >
30 | :
31 |
32 | }
33 |
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 |
18 |
19 |
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 |
30 | >
31 | :
32 |
33 | }
34 |
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 |
22 |
23 | All
24 |
25 | Liked
26 | Watch Later
27 |
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 |
22 |
23 | All
24 |
25 | Liked
26 | Watch Later
27 |
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 |
22 |
23 | All
24 |
25 | Liked
26 | Watch Later
27 |
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 |
32 | >
33 | :
34 |
35 | }
36 |
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 |
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 |
44 | >
45 | :
46 |
47 | }
48 |
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 |
44 | >
45 | :
46 |
47 | }
48 |
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 |
45 | >
46 | :
47 |
48 | }
49 |
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 |
45 | >
46 | :
47 |
48 | }
49 |
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 |
45 | >
46 | :
47 |
48 | }
49 |
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 |
45 | >
46 | :
47 |
48 | }
49 |
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 |
45 | >
46 | :
47 |
48 | }
49 |
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 |
44 | >
45 | :
46 |
47 | }
48 |
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 |
45 | >
46 | :
47 |
48 | }
49 |
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 |
45 | >
46 | :
47 |
48 | }
49 |
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 | You need to enable JavaScript to run this app.
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 |
47 | >
48 | :
49 |
50 | }
51 |
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 | ID
10 | NAME
11 | SAVE TYPE
12 |
13 |
14 |
15 | {data &&
16 | data
17 | .filter((item) => item.saveType.includes(saveType))
18 | .map((item) => (
19 |
23 | {item.mediaId}
24 |
25 |
37 | {item.title}
38 |
39 |
40 |
41 | {item.saveType}
42 |
43 |
44 | ))}
45 |
46 |
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 |
53 | >
54 | :
55 |
56 | }
57 |
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 | 
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)
--------------------------------------------------------------------------------
/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 | dispatch(removeUser())}
48 | data-cy="user-profile-logout-button"
49 | >
50 | Logout
51 |
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 | YEAR
11 | NAME
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 |
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 |
35 | ) : (
36 | -
37 | )}
38 |
39 |
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 |
62 |
63 | ))}
64 |
65 |
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 | YEAR
11 | NAME
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 |
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 |
35 | ) : (
36 | -
37 | )}
38 |
39 |
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 |
62 |
63 | ))}
64 |
65 |
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 | YEAR
11 | NAME
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 |
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 |
35 | ) : (
36 | -
37 | )}
38 |
39 |
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 |
62 |
63 | ))}
64 |
65 |
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 | Refresh
79 | Goto Homepage
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 | YEAR
11 | NAME
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 |
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 |
32 | ) : (
33 | -
34 | )}
35 |
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 |
63 |
64 | ))}
65 |
66 |
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 |
41 | {seasons.seasons.map((option) => (
42 |
47 | {option.name}
48 |
49 | ))}
50 |
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 |
32 |
33 | {/* Tab Header */}
34 |
38 |
45 | KNOWN FOR
46 |
47 |
54 | CREDITS
55 |
56 |
63 | PHOTOS
64 |
65 |
66 |
67 |
68 | {/* Known For */}
69 |
70 |
75 |
76 |
77 | {/* Credits */}
78 |
81 |
82 | {/* Photos */}
83 |
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 |
42 | MOVIES
43 |
44 |
51 | SHOWS
52 |
53 |
60 | PERSON
61 |
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 |
19 |
20 | All
21 |
22 |
23 | Behind the Scenes
24 |
25 |
26 | Teaser
27 |
28 |
29 | Bloopers
30 |
31 |
32 | Clip
33 |
34 |
35 | Featurette
36 |
37 |
38 | Trailer
39 |
40 |
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 |
27 |
28 |
32 |
33 |
34 | {/* Search */}
35 | dispatch(setSearch(!isSearch))}
39 | className="btn bg-transparent hover:bg-transparent focus:bg-transparent m-0 p-0 border-none hover:text-white"
40 | >
41 |
46 |
47 | {/* Movies */}
48 |
52 |
53 |
57 |
58 |
59 | {/* Shows */}
60 |
64 |
65 |
69 |
70 |
71 | {/* Account */}
72 |
76 |
77 |
81 |
82 |
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 |
93 |
94 | {/* Cancel Button */}
95 | {searchParam.get("q") && (
96 |
97 |
101 |
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 |
35 |
36 | All
37 |
38 |
39 | Acting
40 |
41 |
42 | Directing
43 |
44 |
45 | Production
46 |
47 |
48 | Writing
49 |
50 |
51 |
52 |
53 |
54 |
58 | Media
59 |
60 |
64 |
65 | All
66 |
67 |
68 | Movies
69 |
70 |
71 | Shows
72 |
73 |
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 |
13 |
14 |
18 |
19 | Copyright © 2022 - All rights reserved.
20 |
21 |
22 | Made with ❤{" "}
23 |
30 | @helloukey
31 |
32 |
33 |
34 | Data provided by -{" "}
35 |
42 | TMDB
43 |
44 |
45 |
46 |
47 |
112 |
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 |
60 | {/* Actions */}
61 |
66 |
67 |
68 |
72 |
73 |
74 |
78 |
79 |
80 |
81 |
87 |
88 |
92 |
93 |
94 |
95 |
96 | {/* Posters */}
97 |
101 | Photos
102 |
103 |
107 | {data &&
108 | data.map((item, index) => (
109 | handleModal(index)}
114 | data-cy="artist-photos-single"
115 | >
116 |
121 |
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 |
--------------------------------------------------------------------------------