π Table of Contents
- π€ Introduction
- βοΈ Tech Stack
- π Features
- π€Έ Quick Start
- πΈοΈ Snippets
- π Links
I developed a stunning, fully responsive movie website using the TMDB API and React. It features a dynamic Home page for popular and trending movies, detailed movie pages with trailers and cast info, and an Explore section with infinite scrolling.
- React.js
- Redux Toolkit
- Vite
- SASS
π Dynamic Home Page: Showcases popular and trending movies with lazy-loaded images and interactive carousels.
π Detailed Movie Pages: Includes comprehensive information, cast details, trailers, and video clips using a React video player.
π Cast Biographies and Filmographies: Users can explore detailed biographies and filmographies by clicking on cast images.
π Explore Section: Allows users to discover movies and TV series with enhanced infinite scrolling using react-infinite-scroll-component.
π Completely Responsive: Ensures consistent access and optimal viewing on any device with a fully responsive design.
π Global State Management with Redux: Efficient state management across the entire application.
π Where to Watch: Provider logos under the rating row show where a title streams, rents or sells in the visitor's country, with a country picker and a free legal download for public-domain classics from the Internet Archive.
π Languages and Seasons: Language and age rating on each title, trailers filtered by language, an Explore filter for Hindi, Tamil, Telugu and more, and a season and episode browser for TV shows.
π My List: Save titles for later, stored only in the visitor's browser.
π Works on Blocked Networks: TMDB data and images are served through a same-origin proxy on Vercel, so the site loads even where TMDB is blocked and the API token never reaches the browser.
π Search-Friendly: Per-page titles, descriptions and Open Graph tags, server-rendered metadata and Movie/TVSeries structured data for title pages, a dynamic sitemap and robots.txt.
π About and Privacy Pages: Plain-language pages describing the project and exactly what data the site touches, plus an animated 404 page.
Follow these steps to set up the project locally on your machine.
Prerequisites
Make sure you have the following installed on your machine:
Cloning the Repository
git clone https://github.com/satish-kumar75/Movix.git
cd MovixInstallation
Install the project dependencies using npm:
npm installEnvironment Variables
Copy .env.example to .env and fill in your values:
VITE_APP_TMDB_TOKEN=your TMDB API Read Access Token
VITE_APP_VIDSRC_URL=base URL of the embedded player
PROXY_TARGET=https://your-app.vercel.appVITE_APP_TMDB_TOKEN is only read by the serverless functions in api/, never by the browser bundle. PROXY_TARGET is used by the Vite dev server to forward /api and /tmdb-img to your deployed site, so local development works even on networks that block TMDB. Put it in .env.local if you prefer. .env files are git-ignored.
Running the Project
npm run devOpen http://localhost:5173 in your browser to view the project.
Deploying to Vercel
Import the repository, add VITE_APP_TMDB_TOKEN and VITE_APP_VIDSRC_URL under Project Settings β Environment Variables, and deploy. vercel.json already contains the proxy rewrites, the SPA fallback and the security headers, and package.json pins Node 24.
index.scss
:root {
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 1;
font-weight: 500;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
--black: #04152d;
--black2: #041226;
--black3: #020c1b;
--black-lighter: #1c4b91;
--black-light: #173d77;
--pink: #da2f68;
--orange: #f89e00;
--gradient: linear-gradient(98.37deg, #f89e00 0.99%, #da2f68 100%);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: var(--black);
}
::-webkit-scrollbar {
display: none;
}
.skeleton {
position: relative;
overflow: hidden;
background-color: #0a2955;
&::after {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
transform: translateX(-100%);
background-image: linear-gradient(
90deg,
rgba(#193763, 0) 0,
rgba(#193763, 0.2) 20%,
rgba(#193763, 0.5) 60%,
rgba(#193763, 0)
);
animation: shimmer 2s infinite;
content: "";
}
@keyframes shimmer {
100% {
transform: translateX(100%);
}
}
}Img.jsx
import React from "react";
import { LazyLoadImage } from "react-lazy-load-image-component";
import "react-lazy-load-image-component/src/effects/blur.css";
const Img = ({ src, className }) => {
return (
<LazyLoadImage className={className || ""} alt="" effect="blur" src={src} />
);
};
export default Img;mixins.scss
@mixin sm {
@media only screen and (min-width: 640px) {
@content;
}
}
@mixin md {
@media only screen and (min-width: 768px) {
@content;
}
}
@mixin lg {
@media only screen and (min-width: 1024px) {
@content;
}
}
@mixin xl {
@media only screen and (min-width: 1280px) {
@content;
}
}
@mixin xxl {
@media only screen and (min-width: 1536px) {
@content;
}
}
@mixin ellipsis($line: 2) {
display: -webkit-box;
-webkit-line-clamp: $line;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}useFetch.jsx
import { useEffect, useState } from "react";
import { fetchDataFromApi } from "../utils/api";
const useFetch = (url) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
setLoading("loading...");
setData(null);
setError(null);
fetchDataFromApi(url)
.then((res) => {
setLoading(false);
setData(res);
})
.catch((err) => {
setLoading(false);
setError("Something went wrong!");
});
}, [url]);
return { data, loading, error };
};
export default useFetch;package.json
"dependencies": {
"@reduxjs/toolkit": "^1.9.1",
"axios": "^1.2.2",
"dayjs": "^1.11.7",
"react": "^18.2.0",
"react-circular-progressbar": "^2.1.0",
"react-dom": "^18.2.0",
"react-icons": "^4.7.1",
"react-infinite-scroll-component": "^6.1.0",
"react-lazy-load-image-component": "^1.5.6",
"react-player": "^2.11.0",
"react-redux": "^8.0.5",
"react-router-dom": "^6.6.2",
"react-select": "^5.7.0",
"sass": "^1.57.1"
}PlayIcon.jsx
export const PlayIcon = () => {
return (
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
x="0px"
y="0px"
width="80px"
height="80px"
viewBox="0 0 213.7 213.7"
enableBackground="new 0 0 213.7 213.7"
xmlSpace="preserve"
>
<polygon
className="triangle"
fill="none"
strokeWidth="7"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
points="73.5,62.5 148.5,105.8 73.5,149.1 "
></polygon>
<circle
className="circle"
fill="none"
strokeWidth="7"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
cx="106.8"
cy="106.8"
r="103.3"
></circle>
</svg>
);
};Spinner.jsx
import React from "react";
import "./style.scss";
const Spinner = ({ initial }) => {
return (
<div className={`loadingSpinner ${initial ? "initial" : ""}`}>
<svg className="spinner" viewBox="0 0 50 50">
<circle
className="path"
cx="25"
cy="25"
r="20"
fill="none"
strokeWidth="5"
></circle>
</svg>
</div>
);
};
export default Spinner;
// CSS
.loadingSpinner {
width: 100%;
height: 150px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
.spinner {
animation: rotate 2s linear infinite;
z-index: 2;
width: 50px;
height: 50px;
& .path {
stroke: hsl(210, 70, 75);
stroke-linecap: round;
animation: dash 1.5s ease-in-out infinite;
}
}
&.initial {
height: 700px;
}
@keyframes rotate {
100% {
transform: rotate(360deg);
}
}
@keyframes dash {
0% {
stroke-dasharray: 1, 150;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -35;
}
100% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -124;
}
}
}Public Assets used in the project can be found here
