An enterprise-grade, highly optimized, fully decoupled Netflix client clone integrated with Google Gemini AI for contextual movie recommendations. Rather than standard, fragile "clone" scripts, this project demonstrates robust system design, strict SOLID development principles (heavily focusing on the Single Responsibility Principle), browser performance engineering, and modern React design patterns.
You can view the live deployment of this project here: NetflixGemini
Software engineering is about decisions, trade-offs, and deep theory, not just copy-pasting visual elements. My persona is centered around knowing the "why" before writing the "how." In building this application, I prioritized:
- Strict Single Responsibility Principle (SRP): Components are strictly "dumb" visual containers. All heavy side-effects, state transitions, API integrations, and validation logic are completely extracted into customized hooks and centralized store modules.
- Performance as a First-Class Citizen: Managing render trees defensively. This includes preventing the classic "React API Waterfall" anti-pattern, locking down DOM elements from layout shift (CLS), and handling React's object memory comparisons strictly.
- Security & Data Integrity: Designing zero-leak environments. Decoupling sensitive keys, safeguarding route layout bounds on the server-side, preventing flash-of-unauthenticated-content (FOUC), and utilizing immutable models for application schemas.
- Frictionless Standardization: Building components that align with web and React specifications (e.g., using standard prop forwarding rather than customized patterns) ensuring clean library interoperability.
- React 19: Utilizes cutting-edge React features including native
refprop forwarding. - React Router v7: Powers the client-side routing, dividing the application cleanly into isolated layout-driven domains (Public Layout and Protected Layout).
- Tailwind CSS v4: Operates mobile-first layout controls, utilising linear gradients and responsive stacking rules with fluid container adaptations.
- Zustand: Subscribes React components to a lightning-fast state store located entirely outside the React component tree on the heap memory.
- Firebase Authentication & Hosting: Secures user sign-ups, logs user credentials, caches persistent client sessions via IndexedDB listeners, and deploys production static assets.
- Google Gemini API: Generates intelligent, content-aware movie and TV recommendations dynamically.
- TMDB REST API: Feeds complete, paginated catalogs of metadata, movie posters, and high-fidelity video attachments.
- Axios Client: Wraps endpoints in modular pre-stamped envelopes (instances) configured with automatic bearer tokens and base URLs.
- Zod Validator: Intercepts form inputs to validate strings and enforce strict password complexity rules.
src/
├── assets/ # Global styles, static logos, and asset layers
├── components/ # Pure, reusable "dumb" UI components
│ ├── ui/ # Visual elements (Buttons, AuthBackground)
│ ├── forms/ # Visual layout inputs (InputField, PasswordInput)
│ ├── shimmers/ # Layout-stable pulse skeleton loaders
│ └── navigation/ # Contextual headers (PublicNav, ProtectedNav)
├── context/ # Core infrastructure (AuthProvider for auth sync)
├── hooks/ # Custom hooks encapsulating heavy side-effects
│ ├── useAuth/ # Secure wrapper for session states
│ ├── useLoginForm/
│ ├── useSignupForm/
│ ├── useVideoBackground/
│ └── useContentFeed/
├── layouts/ # Rigid layout frame containers (PublicLayout, ProtectedLayout)
├── pages/ # Decoupled routes (LandingPage, LoginPage, BrowsePage)
├── services/ # Pre-configured instances (firebase.js, tmdb.js, gemini.js)
├── store/ # Zustand global state heap (authStore, movieStore)
├── utils/ # Deeply frozen validation dictionaries and shared constants
├── App.jsx # App router configuration and global providers
└── main.jsx # Bootloader rendering React root
Instead of bloating individual pages with security validations or using fragile inline wrappers that evaluate on every route swap, the routing was designed around nested layout domains.
PublicLayoutacts as the application's "Bouncer": It checks if a session is present. If yes, it forcefully redirects the user to the private space (/app), preventing authenticated users from unnecessarily reloading forms.ProtectedLayoutacts as the "Guard": It intercepts non-authenticated guests and redirects them instantly to/loginusing<Navigate replace />, completely wiping out their transition history stack so the browser's "Back" button cannot navigate back to private assets.
// App.jsx (Infrastructure Configuration Provider)
const appRouter = createBrowserRouter([
{
path: '/',
element: <PublicLayout />, // Bouncer
children: [
{ index: true, element: <LandingPage /> },
{ path: 'login', element: <LoginPage /> },
{ path: 'signup', element: <SignupPage /> }
]
},
{
path: '/app',
element: <ProtectedLayout />, // Guard
children: [
{ index: true, element: <BrowsePage /> },
{ path: 'movie/:movieId', element: <MoviePage /> },
{ path: 'account', element: <AccountPage /> }
]
}
]);The Performance "Why": Since layout frames wrap nested child routes, when navigating between /app children, React Router re-renders only the <Outlet /> element. The parent layout, its security controls, and the complex stateful navigation bars remain mounted, preventing visual flicker and avoiding repetitive security checks.
To prevent the nested DOM "Provider Wrapper Hell" inherent to React Context, state was migrated to Zustand.
- Philosophy Comparison: RTK relies on a complex, bureaucratic "Flux" architecture requiring separate slice definitions, redundant action dispatch, and dispatch/reducer handshakes on every simple state change. Zustand operates a Pub/Sub (Publish-Subscribe) pattern, maintaining states entirely outside the React component tree on the memory heap. It leverages React 18's native
useSyncExternalStorehook to bridge updates safely. - Selector Optimization & Render Mitigation: To secure optimal rendering speeds, components strictly query states using precise selectors. Furthermore, Zustand’s
useShallowhook was implemented to memoize multi-key extractions.
// Prevents unneeded component re-renders when unrelated properties in the store change.
const { nowPlayingMovies, trendingMovies, topRatedMovies } = useMovieStore(
useShallow((state) => ({
nowPlayingMovies: state.nowPlayingMovies,
trendingMovies: state.trendingMovies,
topRatedMovies: state.topRatedMovies
}))
);Fetching catalogs sequentially creates a critical performance roadblock known as the "API Request Waterfall" (where each request must wait to finish before the next one starts).
To solve this, a custom useContentFeed hook orchestrates parallel network queries, retrieving multiple categories (Trending, Top Rated, Action, Comedy) simultaneously through a unified Promise.all operation.
// useContentFeed.js
const categoriesToFetch = [
{ storeKey: "trendingMovies", endpoint: requests.fetchTrending },
{ storeKey: "topRatedMovies", endpoint: requests.fetchTopRated },
{ storeKey: "actionMovies", endpoint: requests.fetchActionMovies }
];
const loadAllCategories = async () => {
try {
// 1. Fire all requests concurrently (Parallel)
const apiPromises = categoriesToFetch.map(category => tmdb.get(category.endpoint));
// 2. Wait concurrently for all resolutions
const results = await Promise.all(apiPromises);
// 3. Map responses back to respective store keys maintaining list order
results.forEach((response, index) => {
const storeKey = categoriesToFetch[index].storeKey;
setMovieCategory(storeKey, response.data.results);
});
} catch (error) {
console.error("Critical parallel data ingestion failure", error);
}
};The Performance "Why": Assuming each endpoint takes 300ms, a sequential approach results in a 900ms wait. The parallel pipeline resolves all datasets concurrently, capping total network delay at the speed of the single slowest query (~300ms), representing a 75% reduction in loading latency.
Embedding YouTube's standard iframe player inside a hero background creates major challenges: interactive players pause on mouse clicks, black borders flash, and YouTube controls clutter the streaming aesthetic.
To solve this, we designed a customized HeroVideoBackground player using an Unbreakable CSS Math Hack to force raw iframes to scale and behave like standard background images (object-fit: cover) across all screen viewports (such as iPads).
// HeroVideoBackground.jsx
function HeroVideoBackground({ movieId }) {
const { youTubeKey } = useVideoBackground(movieId);
if (!youTubeKey) return null;
return (
<div className="absolute w-full aspect-video overflow-hidden">
<iframe
className="absolute top-1/2 left-1/2 pointer-events-none border-0
w-[100vw] h-[56.25vw]
min-h-[100vh] min-w-[177.77vh]
-translate-x-1/2 -translate-y-1/2 scale-[1.35]"
src={`https://www.youtube.com/embed/${youTubeKey}?autoplay=1&mute=1&controls=0&modestbranding=1&rel=0&loop=1&playlist=${youTubeKey}&cc_load_policy=0&iv_load_policy=3`}
title="Cinematic Trailer"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
/>
{/* Dynamic Linear Gradient Overlays for Seamless UI Integration */}
<div className="absolute inset-0 bg-linear-to-r from-black/80 via-black/40 to-transparent" />
<div className="absolute inset-0 bg-linear-to-t from-black from-10% via-black/60 via-30% to-transparent to-70%" />
</div>
);
}- Aspect Ratio Alignment (
w-[100vw] h-[56.25vw]): Locks down the dimensions to the perfect16:9widescreen aspect ratio on laptops and wider displays. - Tall Screen Scaling (
min-h-[100vh] min-w-[177.77vh]): If a user loads the app on a narrow, tall viewport (like an iPad), the iframe's height would normally drop below the container's height, creating ugly black borders. This minimum lock forces the iframe's height to cover the viewport, expanding its width proportionally (16/9 = 1.7777, hence177.77vh). - The Crop Zoom (
scale-[1.35] pointer-events-none): Zooms in on the center of the video, pushing YouTube's embedded branding title blocks and interactive layers completely off the screen bounds while making clicks slide straight through to the controls. - Tailwind v4 Seamless Gradient Stops (
from-10% to-70%): Avoids sub-pixel rendering gaps (1px bleed lines) by enforcing 100% solid black coloration up to 10% height, softly fading out to transparent at 70% height to blend perfectly with the catalog list container below.
We decoupled form presentation entirely from the underlying execution systems:
- React 19 Native Refs: Because this project uses React 19, the legacy
forwardRef()boilerplate is completely deprecated. Standardrefvariables are extracted directly from props like standard parameters, simplifying component communication. - Uncontrolled Performance Inputs: Form fields (
InputField,PasswordInput) useuseRefbridges rather than firing a state-based component update on every single keypress, removing useless re-render loops during input entries. - Immutable Error Configurations: Validation rules are driven by Zod schema primitives, but error strings are centrally consolidated inside a deeply frozen JavaScript dictionary to secure runtime immutability.
// utils/validationErrors.js
export const VALIDATION_ERRORS = Object.freeze({
NAME_REQUIRED: "Name is required.",
NAME_TOO_SHORT: "Name must be at least 2 characters.",
EMAIL_INVALID: "Please enter a valid email.",
PASSWORD_TOO_SHORT: "Must be at least 8 characters.",
PASSWORD_NO_UPPER: "Add at least one uppercase letter."
});
// schemas/authSchema.js
export const namePrimitive = z.string()
.min(2, VALIDATION_ERRORS.NAME_TOO_SHORT)
.transform(val => val.trim())
.refine(
val => /^[\p{L}](?:[\p{L}'\s.-]*[\p{L}.])?$/u.test(val), // Unicode/Initials Friendly
VALIDATION_ERRORS.NAME_INVALID_FORMAT
);The Re-render Trap Defeated: When error states are empty, resetting state to an empty object {} normally triggers a re-render in React, because objects are compared by memory references ({} === {} is false). We implemented strict guard clauses that evaluate object key length before scheduling updates, preventing unnecessary paint cycles:
if (Object.keys(formErrors).length > 0) {
setFormErrors({}); // Guard clause: prevents useless re-renders on subsequent clean submissions
}- Advanced Authentication Domains: Divides routes strictly into guest spaces and private spaces using nested layout-driven "guards" and "bouncers."
- Gemini AI-Powered Recommendations: Seamlessly suggests movies and TVs dynamically based on contextual queries.
- Parallel Catalog Ingestion: Downloads multiple categories concurrently, eliminating screen load times.
- Cinematic Widescreen Background Backgrounds: Forces standard YouTube iframes to act as flawless, responsive cinematic overlays without clickable interfaces.
- Layout-Stable Shimmering Pulse Loaders: Generates glowing placeholders matching the exact card dimensions of incoming datasets, preventing visual Layout Shift (CLS).
- Dynamic Custom Eye-Toggles: Includes high-fidelity secure password masking without violating the Single Responsibility Principle.
- Interactive Mobile Responsive Profile Menus: Houses customizable account drop-downs that automatically reposition navigation options onto responsive slides on smaller screens.
- Click-to-Close Interceptors: Integrates fullscreen invisible overlay backdrops to capture clicks outside drop-down components safely without bloating document event listeners.
Make sure you have Node.js installed and a valid API Account with TMDB and Firebase.
-
Clone the Repository:
git clone https://github.com/your-username/netflix-gemini.git cd netflix-gemini -
Install Production Dependencies:
npm install
-
Secure Your Environment Credentials: Create a
.env.localfile in the root of the project to securely house your integration keys (this file is excluded from git commits through.gitignoreto prevent leaks):# Vite Environment Prefix Configuration VITE_FIREBASE_API_KEY="your_firebase_api_key" VITE_FIREBASE_AUTH_DOMAIN="your_firebase_auth_domain" VITE_FIREBASE_PROJECT_ID="your_firebase_project_id" VITE_FIREBASE_STORAGE_BUCKET="your_firebase_storage_bucket" VITE_FIREBASE_MESSAGING_SENDER_ID="your_firebase_sender_id" VITE_FIREBASE_APP_ID="your_firebase_app_id" # TMDB Integration VITE_TMDB_ACCESS_TOKEN="your_tmdb_bearer_token" VITE_TMDB_IMG_BASE_URL="https://image.tmdb.org/t/p/original" # Gemini AI Configuration VITE_GEMINI_API_KEY="your_gemini_api_key"
-
Launch the Optimized Dev Server:
npm run dev
The project is configured for lightweight, optimized static builds deployed onto Firebase Hosting. Because this is a Single Page Application (SPA), the Firebase hosting module rewrite ensures all incoming URLs are delegated back to index.html so React Router can process path resolution.
-
Compile & Package Assets:
npm run build
This triggers Vite to compile, package, and optimize all React/JS code into optimized chunks inside the
/distdirectory. -
Single-Page Router Rewrite Rules (
firebase.json):{ "hosting": { "public": "dist", "ignore": [ "firebase.json", "**/.*", "**/node_modules/**" ], "rewrites": [ { "source": "**", "destination": "/index.html" } ] } } -
Publish Production Build:
firebase deploy
- GitHub: @monukd01dev
- LinkedIn: Monu Kumar
Crafted with ❤️, clean code, and zero compromises.