Skip to content

Conversation

emirbayraktdev
Copy link

🎬 CinEmir — TMDB Movie Explorer

A sleek, fast movie browser built with React + TypeScript + Vite, styled with Tailwind, and powered by TMDB. It features a dynamic, cinematic UI, smooth navigation, rich details, and a polished mobile experience.

“Find your next favorite film faster — with style.”


🌐 Live Demo

👉 cinemir.vercel.app


✨ Highlights

  • Smart search with debounce, grid results, and “Load more”
  • Cinematic detail page: hero with backdrop + gradient, ratings, and key facts
  • Tabbed details (synced to URL): Overview / Videos / Photos
  • Cast grid (with profile placeholders) & More like this rail (drag-to-scroll)
  • Robust states: skeletons, graceful errors (with retry), and empty views
  • A11y & UX polish: keyboard focus, CLS‑safe images
  • Sticky header & well-designed footer
  • Document title hook (tab shows the movie title)
  • Scroll behavior: scroll-to-top on route change, but not on “Load more” or tab switch
  • Decent amount of tests (limited due to time constraints): Vitest unit tests + Playwright E2E

🧱 Tech Stack

  • Frontend: React, TypeScript, Vite
  • Routing: React Router
  • Styles: Tailwind CSS, lucide-react icons
  • API: TMDB (The Movie Database)
  • Testing: Vitest + @testing-library/react + Playwright

📦 Getting Started

# 1) Install deps
npm install

# 2) Set env (see next section)
cp .env.example .env
# then set VITE_TMDB_API_KEY=your_key_here

# 3) Run dev server
npm run dev

# 4) Build & preview
npm run build
npm run preview

🔐 Environment Variables

Create .env from the example and add your TMDB key:

VITE_TMDB_API_KEY=YOUR_TMDB_KEY

This product uses the TMDB API but is not endorsed or certified by TMDB.


🧪 Testing

Unit: Vitest + Testing Library

  • Configured JSDOM environment
  • Global test utils (RTL, user-event), JSX transform, and strict TypeScript

Run:

npm run test          # watch
npm run test:run      # single run (CI)
npm run test:ui       # if using the Vitest UI

Folder convention:

tests/
  unit/
    *.test.tsx
  e2e/
    *.spec.ts

E2E: Playwright

  • Headless-stable tests with API mocking (routes for TMDB endpoints)
  • Local dev server bootstrapped via Playwright webServer

Run:

npm run e2e        # headless
npm run e2e:open   # headed (debug)

Tip: For headless reliability, don’t use arbitrary timeouts — mock network and assert on locators (we do).


🌐 Deployment (Vercel)

This is a client-side SPA; add a rewrite so all routes serve index.html:

vercel.json

{
    "rewrites": [{ "source": "/(.*)", "destination": "/" }]
}

Then:

  1. Push your branch to GitHub (e.g., assignment).
  2. In Vercel, import the repo and select the assignment branch (Project → Settings → Git → Production Branch) or create a Preview for the branch.
  3. Set VITE_TMDB_API_KEY in Project → Settings → Environment Variables.
  4. Build Command: vite buildOutput: dist

If you saw the Vercel default 404 page, you were missing the rewrite above.


🔧 Developer Notes

  • Reduced CLS: explicit image width/height; reserved spaces in skeletons; hero has fixed height
  • URL tab sync: ?tab=overview|videos|photos (resets to overview when navigating to a new movie)
  • Images endpoint & language: the images API can return empty arrays when language filters; we fetch without forcing language there to always get assets.
  • Scroll restoration: customized so pagination/tab switches don’t yank the viewport.

👏 Credits

  • Data/API: TMDB
    • This product uses the TMDB API but is not endorsed or certified by TMDB.
  • Icons: lucide.dev

📄 License

MIT — do what you love. 💚

…; add base dark layout (Header/Footer) and routes
…onents; wire global ErrorBoundary; install lucide-react icons
… hero dimensions; add custom hook for document title management
…lToTop component for improved navigation; update PillTabs styling; refine MovieHero gradients and loading behavior; adjust scrollbar styles for better aesthetics
…HomeRails for improved structure; add SearchResultsSection for handling search results; implement useTmdbSearch hook for data fetching and error handling
…rgins and paddings for improved responsiveness and aesthetics
…improve layout; implement custom hook for document title management; update storyline heading style in OverviewSection
…jects; clean up MovieHeroSkeleton and update test assertions for clarity

url.search = search.toString();

const res = await fetch(url.toString(), init);
Copy link
Contributor

@snewell92 snewell92 Aug 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nowadays I would want to use react-query and build the tmdb lib around useQuery primitives with hooks like useMovies and useMovieDetails or something like that, you get caching OOTB that way.

setLoading(true);
setError(null);

fetchJson<SearchResponse>(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the loading around here (loading / error) can be subsumed into react-query (now called tanstack query), as the useQuery hook implements all of this for you.

You would like build your query key for useQuery with the page and page size in mind.

);
})
.finally(() => {
if (!controller.signal.aborted) setLoading(false);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's very nice seeing someone be aware of and use abort controllers!

<div className="absolute inset-0">
{heroBg ? (
<div
className="absolute inset-0 bg-cover bg-center blur-sm scale-105"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gotta love tailwind

: "bg-[#00ad99] hover:bg-[#009b89]"
}
text-white disabled:opacity-60
`}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think i'd recommend looking at clsx and the full tailwind utility suite. Shadcn also uses these utilities, it makes conditional classes behave without messy nested logic like this. So you could refacotr this bit into:

<button
  className={cn(
    loading
      ? "bg-[#00ad99]/40 cursor-not-allowed"
      : "bg-[#00ad99] hover:bg-[#009b89]",
    "inline-flex items-center justify-center gap-2 rounded-lg px-5 py-2 text-lg font-medium transition-colors duration-200 text-white disabled:opacity-60"
  )}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Set up for a cn function is documented here: https://ui.shadcn.com/docs/installation/manual


type SearchResponse = TmdbPage<TmdbMovie>;

export function useTmdbSearch(query: string, page: number) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great intuition to pull this hook out - having it be useQuery inside will erase a lot of the complexity too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants