A fast, modern movie discovery platform powered by the TMDB API — featuring curated browsing, advanced search with multi-dimensional filters, rich detail pages, and a cinema-grade dark UI.
Cinescope is a production-grade movie discovery SPA that connects directly to The Movie Database (TMDB) API, giving users access to a library of hundreds of thousands of films — browsable by popularity, recency, and critical acclaim, and searchable with precise multi-dimensional filters.
The goal was to build something that feels like a polished streaming platform's discovery section: visually rich, responsive across every device, and fast enough that it never gets in the way of the experience.
Who is it for?
- Movie enthusiasts who want to explore films beyond algorithmic recommendations
- Developers looking for a reference implementation of a well-structured React + TanStack Query application
- Portfolio showcases demonstrating real-world API integration and modern frontend architecture
- Curated Home Feed — Four live movie categories side-by-side: Now Playing, Popular, Top Rated, and Upcoming, each backed by a real-time TMDB feed
- Hero Banner — Prominent featured movie at the top of the home page with backdrop, metadata, and navigation CTAs
- Horizontal Carousels — Smooth, scrollable movie rows per category with consistent card design
- Full-Text Movie Search — Debounced (400ms) search against the TMDB search endpoint as-you-type
- Genre Filter — Filter by any of TMDB's official genre classifications
- Release Year Filter — Narrow results to a specific production year
- Rating Filter — Set a minimum audience score threshold
- Sort Control — Sort by popularity, release date, vote average, or vote count — ascending or descending
- Filter Composition — All filters compose together; switching from search to browse mode swaps endpoints automatically
- Full Backdrop Presentation — Cinematic backdrop with gradient overlay
- Comprehensive Metadata — Title, tagline, rating, genres, release date, runtime, spoken language, original language, director, budget, and revenue
- Dynamic Rating Badge — Color-coded score display (green / yellow / red) based on audience rating
- Full Cast Carousel — Top 10 cast members with profile photos and character names
- Similar Movies — Contextual recommendations pulled from TMDB's similarity engine
- External Links — Direct link to the movie's official website when available
- Dark Mode by Default — System-preference-aware theme with localStorage persistence and manual toggle
- Fully Responsive Layout — Sidebar navigation on desktop; slide-out drawer on mobile
- Skeleton Loading States — Animated skeletons on every loading surface — no layout shift
- Error States with Retry — Network failures surface actionable error cards
- Empty States — Contextual zero-result messaging per screen
- Code-Split Routes — Each page loads lazily via React
Suspensefor faster initial paint
- Strict TypeScript — Full strict mode with no
anyescapes; all TMDB response shapes are typed - Centralized Query Keys — Factory pattern for all TanStack Query cache keys prevents key collisions
- URL-Synced Filters — Search filters are stored in and read from URL search params — shareable and browser-history-aware
- Path Aliases —
@/maps tosrc/throughout the codebase
| Layer | Technology | Version |
|---|---|---|
| UI Framework | React | 18.3 |
| Language | TypeScript | 6.0 |
| Build Tool | Vite | 8.0 |
| Routing | React Router DOM | 6.30 |
| Server State | TanStack React Query | 5.101 |
| HTTP Client | Axios | 1.17 |
| Styling | Tailwind CSS | 3.4 |
| UI Primitives | Radix UI (Select, Slot, Separator) | 2.x |
| Icons | Lucide React | 1.18 |
| CSS Utilities | clsx · tailwind-merge · class-variance-authority | latest |
| External API | TMDB (The Movie Database) | v3 |
cinescope/
├── index.html # App entry point (dark class applied at root)
├── vite.config.ts # Vite + path alias configuration
├── tailwind.config.js # Design tokens and dark mode config
├── tsconfig.json # Strict TypeScript configuration
│
└── src/
├── main.tsx # ReactDOM root mount
├── App.tsx # Router setup + lazy-loaded routes + Suspense
├── index.css # Global styles, CSS custom properties, utilities
│
├── api/
│ ├── axiosClient.ts # Axios instance — base URL + API key injection
│ └── movies.ts # All TMDB endpoint functions
│
├── components/
│ ├── layout/
│ │ ├── AppShell.tsx # Top-level flex layout wrapper
│ │ ├── Sidebar.tsx # Desktop navigation
│ │ └── Header.tsx # Mobile header + drawer toggle
│ │
│ ├── movie/
│ │ ├── HeroBanner.tsx # Featured movie hero section
│ │ ├── MovieCard.tsx # Individual movie card (lazy image load)
│ │ ├── MovieCardSkeleton.tsx # Card loading placeholder
│ │ ├── MovieGrid.tsx # Responsive grid with loading/error states
│ │ ├── MovieRow.tsx # Horizontal scrollable carousel
│ │ ├── RatingBadge.tsx # Color-coded rating display
│ │ └── GenreBadge.tsx # Genre tag chip
│ │
│ ├── shared/
│ │ ├── EmptyState.tsx # Zero-results UI
│ │ └── ErrorState.tsx # Error UI with optional retry
│ │
│ └── ui/ # Headless-first primitives (shadcn/ui style)
│ ├── button.tsx
│ ├── input.tsx
│ ├── select.tsx
│ ├── badge.tsx
│ └── skeleton.tsx
│
├── hooks/ # One hook per data concern
│ ├── useNowPlaying.ts
│ ├── usePopularMovies.ts
│ ├── useTopRated.ts
│ ├── useUpcoming.ts
│ ├── useSearchMovies.ts # Switches between /search and /discover based on filters
│ ├── useMovieDetail.ts
│ ├── useMovieCredits.ts
│ ├── useSimilarMovies.ts
│ ├── useGenres.ts # Infinite staleTime — never refetches genres
│ ├── useMovieFilters.ts # URL search-param–backed filter state
│ └── useDebounce.ts # 400ms debounce for search input
│
├── lib/
│ ├── constants.ts # Query key factory, image URL builders, filter options
│ └── utils.ts # cn(), formatDate(), formatCurrency(), getRatingBg()
│
├── pages/
│ ├── HomePage.tsx # Hero + four movie-row categories
│ ├── SearchPage.tsx # Search bar + filter bar + paginated grid
│ ├── MovieDetailPage.tsx # Full detail view with cast and similar movies
│ └── NotFoundPage.tsx # 404 fallback
│
├── providers/
│ ├── QueryProvider.tsx # TanStack Query client config (staleTime, retries, GC)
│ └── ThemeProvider.tsx # Theme context + localStorage + system preference
│
└── types/
└── index.ts # TypeScript interfaces for all TMDB response shapes
Key architectural boundaries:
api/— raw TMDB calls only; no business logichooks/— TanStack Query wrappers; data fetching lives here, not in componentscomponents/— purely presentational; consume hooks via props or direct callspages/— composition layer; assembles components and hooks into full viewslib/— pure utility functions and constants; zero side-effects
- Node.js 18+
- A free TMDB API key
git clone https://github.com/engraya/Cinescope.git
cd cinescopenpm installcp .env.example .envOpen .env and add your TMDB API key:
VITE_TMDB_API_KEY=your_tmdb_api_key_here
VITE_TMDB_BASE_URL=https://api.themoviedb.org/3
VITE_TMDB_IMAGE_BASE_URL=https://image.tmdb.org/t/pnpm run devThe app will be available at http://localhost:5173.
npm run build # TypeScript compile + Vite bundle
npm run preview # Preview the production build locally| Variable | Required | Description |
|---|---|---|
VITE_TMDB_API_KEY |
Yes | Your TMDB v3 API key — obtain from themoviedb.org/settings/api |
VITE_TMDB_BASE_URL |
No | TMDB API base URL. Defaults to https://api.themoviedb.org/3 |
VITE_TMDB_IMAGE_BASE_URL |
No | TMDB image CDN base URL. Defaults to https://image.tmdb.org/t/p |
All Vite environment variables must be prefixed with
VITE_to be accessible in the browser bundle.
Cinescope integrates exclusively with the TMDB API v3.
| Method | Endpoint | Purpose |
|---|---|---|
GET |
/movie/now_playing |
Currently in-theater movies |
GET |
/movie/popular |
Trending movies by popularity score |
GET |
/movie/top_rated |
Highest-rated movies of all time |
GET |
/movie/upcoming |
Movies with upcoming release dates |
GET |
/search/movie |
Full-text title search |
GET |
/discover/movie |
Filter-driven discovery (genre, year, rating, sort) |
GET |
/movie/:id |
Full movie detail |
GET |
/movie/:id/credits |
Cast and crew for a movie |
GET |
/movie/:id/similar |
Contextual similar-movie recommendations |
GET |
/genre/movie/list |
Complete genre taxonomy |
The useSearchMovies hook automatically switches between /search/movie and /discover/movie based on whether the user has typed a title query. This allows filters to work in both search and browse contexts without any manual mode management.
// Poster images (poster_path)
posterUrl(path, 'w500') // → https://image.tmdb.org/t/p/w500/path.jpg
// Backdrop images (backdrop_path)
backdropUrl(path, 'w1280') // → https://image.tmdb.org/t/p/w1280/path.jpg
// Actor profile photos (profile_path)
profileUrl(path, 'w185') // → https://image.tmdb.org/t/p/w185/path.jpgAvailable sizes: w92, w154, w185, w342, w500, w780, original for posters; w300, w780, w1280, original for backdrops.
TanStack Query is configured with aggressive caching that suits read-heavy, slowly-changing API data:
{
staleTime: 5 * 60 * 1000, // Data stays fresh for 5 minutes
gcTime: 10 * 60 * 1000, // Unused cache entries purged after 10 minutes
retry: 2, // Retry failed requests twice before surfacing error
}Genres are special-cased with staleTime: Infinity — the genre list almost never changes, so it's fetched once per session.
Every page component is lazy-loaded via React's lazy() + Suspense. The initial bundle only contains the router, providers, and layout shell — each route's code is fetched on demand.
const HomePage = lazy(() => import('@/pages/HomePage'))
const SearchPage = lazy(() => import('@/pages/SearchPage'))
const MovieDetailPage = lazy(() => import('@/pages/MovieDetailPage'))MovieCard uses native loading="lazy" on poster images, deferring off-screen image fetches until the user scrolls near them. Combined with TMDB's global CDN, images load fast without any extra infrastructure.
The search input is debounced at 400ms via useDebounce. This prevents a new API request on every keystroke while keeping the search feel responsive.
Filters are stored in URL search parameters via useMovieFilters, which means browser back/forward navigation works naturally — no extra state hydration required.
Replace these placeholders with actual screenshots from the running application.
UI primitives in components/ui/ follow the shadcn/ui pattern: Radix UI handles behavior and accessibility, Tailwind + CVA (class-variance-authority) handles visual variants. This makes it trivial to add new variants without forking the primitive.
All cache keys are defined in lib/constants.ts using a factory pattern:
export const queryKeys = {
movies: {
all: () => ['movies'] as const,
lists: () => [...queryKeys.movies.all(), 'list'] as const,
popular: (page: number) => [...queryKeys.movies.lists(), 'popular', page] as const,
detail: (id: number) => [...queryKeys.movies.all(), 'detail', id] as const,
credits: (id: number) => [...queryKeys.movies.all(), 'credits', id] as const,
similar: (id: number) => [...queryKeys.movies.all(), 'similar', id] as const,
},
genres: () => ['genres'] as const,
}This prevents cache key collisions and makes invalidation predictable.
Every TMDB response shape is defined in src/types/index.ts. Axios calls are typed with generics (axios.get<PaginatedMovies>(...)) so TypeScript catches shape mismatches at compile time rather than runtime.
The ThemeProvider writes a dark or light class to document.documentElement which gates Tailwind's dark: variants. The app defaults to dark in index.html to avoid a flash-of-light-theme on load.
Ideas for meaningful next additions, grounded in what the codebase already supports:
- Watchlist / Favorites — Client-side persistence via
localStorageor a lightweight backend; theMovieCardUI already has an "Add to Favorites" affordance - TV Show Support — TMDB's
/tvendpoints mirror/movieexactly; the type system and hooks would extend cleanly - Infinite Scroll — Replace page-based pagination on
SearchPagewith TanStack Query'suseInfiniteQuery - Trailers & Media — TMDB exposes
/movie/:id/videosfor YouTube trailers; embed in the detail page - Person Pages — Director and cast member profile pages using
/person/:id - Multi-language Support — TMDB supports
language=on every endpoint; add ani18nlayer - PWA Support — Add a service worker and web manifest to support offline browsing of cached movies
Contributions are welcome. To keep the codebase consistent, please follow these conventions:
- Fork the repo and create a feature branch from
main - Install dependencies with
npm install - Develop against
npm run dev - Type-check before committing:
npx tsc --noEmit - Keep PRs focused — one concern per pull request
- Follow the existing patterns: new data concerns → new hook; new API call →
api/movies.ts; new UI primitive →components/ui/
git checkout -b feature/your-feature-name
# make your changes
git commit -m "feat: describe the change"
git push origin feature/your-feature-name
# open a pull requestBuilt with React · Vite · TanStack Query · Tailwind CSS
Data provided by The Movie Database (TMDB) · This product uses the TMDB API but is not endorsed or certified by TMDB.