CineVault
Team Collaboration + Code Walkthrough
3 developers · GitHub history · curriculum proof · beginner-ready speaking notes
⚛️ React + Vite 🗺 React Router v6 🌐 Axios + TMDB API 🎛 shadcn/ui + Tailwind 💾 JSON Server Backend 📋 Full CRUD Watchlist 🌿 Branch + commit evidence 🧠 Beginner presenter script
URL

Open CineVault Live

Vercel deployment Presentation-ready

Scan or click the live app

Use this slide when the audience wants to open the project quickly. The QR code points to the deployed CineVault app, and the URL below is clickable during presentation mode.
https://cine-vault-tan.vercel.app/
Ready to say: "This is the deployed version. You can scan the QR code or open the URL directly to see the same app we are explaining in the code walkthrough."
GitHub

Team GitHub Profiles

Ali Saad GitHub avatar
Ali Saad
@aliihsaad
Interface architecture, watchlist flow, Search UI improvements.
--Repos
--Followers
--Following
Open GitHub
Loading public GitHub profile data...
Noah Perez GitHub avatar
Noah Perez
@NoahPerez
Data foundation, TMDB flow, detail pages, search feature.
--Repos
--Followers
--Following
Open GitHub
Loading public GitHub profile data...
Almas Khan GitHub avatar
Almas Khan
@Almas-Eclipse
Genre discovery, profile dashboard, loading and route-safety fixes.
--Repos
--Followers
--Following
Open GitHub
Loading public GitHub profile data...
Ready to say: "These cards link directly to each teammate's GitHub profile. When online, they also load public profile details like avatar, bio, public repositories, and followers."
Proof

What Was Checked Before Rebuilding This Deck

Project files

  • day-1 directory: presentation files, roadmap files, guide files, architecture PDF, backend folder.
  • CineVault repo: src pages, components, context, API helpers, package files, deploy config.
  • Tracked footprint: 80 repo files from the local file inventory.

GitHub history

  • Branch list: local and remote branches checked with git branch -a.
  • Graph: merge history checked with git log --graph --decorate --all.
  • Authorship: commit counts and file touch lists checked per teammate.

Curriculum summaries

  • Module 1: weeks 1 to 6 summary files reviewed.
  • Module 2: weeks 1 to 5 summaries plus week 6 project notes reviewed.
  • Goal: separate course-covered methods from beyond-course decisions.
Ready to say: "This presentation is not built from memory. It is rebuilt from the files, the commit graph, the branch list, and the curriculum summaries."
Graph

Branch Graph — How The Team Work Was Split

Remote branches reviewed

BranchFeature signal
origin/sidebarnavigation layout foundation
origin/sidebar-iconssidebar visual/icon upgrade
origin/hero-bannerhero component + movie/TV page split
origin/watchlistwatchlist CRUD and context structure
origin/page/details-pagemovie and TV detail sections
origin/feature/episodes-listepisodes list and context refactor
origin/profile-dashboardprofile recommendations dashboard
origin/feat/searchsearch page and comments
origin/search-ui-improvementssearch page UI polish merged in PR #16

Commit graph reading

→ Feature branch starts from main.
→ One teammate implements a focused feature.
→ Pull request merges feature into main.
→ Later branches build on the merged result.
→ Merge commits show collaboration order, not only final code.
Ready to say: "The graph tells the project story. Sidebar and layout came early, then hero/detail/watchlist, then profile, episodes, bug fixes, search, and deployment."
Contributors

Contribution Numbers — What The History Shows

Raw non-merge commits

AuthorCount
Noah17
Ali Saad15
Almas Khan7
Total: 39 non-merge commits checked after PR #16.

Unique file touches

AuthorFiles
Noah88 unique paths
Ali49 unique paths
Almas29 unique paths
This includes historical setup files and final src files, so it is evidence, not a ranking.

Identity note

  • Noah appears as the main author stream in shortlog and also as early commit names in the detailed log.
  • The presentation treats those commits as one teammate story.
  • This avoids over-crediting or under-crediting because of Git identity formatting.
Ready to say: "We compare real outputs: commits, branches, files, and behavior. The point is not who typed more lines; the point is how the team delivered a working app together."
History

Feature Timeline — What Arrived In What Order

Early structure

Commit / branchMeaning
initial commitsVite app, package setup, starter files
080b7e8MovieContext and homepage popular movies
origin/sidebarlayout and navigation shell
origin/sidebar-iconsTV route and icon improvement
origin/genregenre page and footer work

Feature completion

Commit / branchMeaning
origin/hero-bannermovie/TV hero and detail route foundations
origin/watchlistwatchlist create/read/update/delete flow
origin/page/details-pagecast, gallery, reviews, overview sections
origin/profile-dashboardprofile stats and recommendations
origin/tv-episodes-uiepisode guide UI polish
origin/feat/searchsearch page and final comments
PR #16 search-ui-improvementsSearch.css added and Search.jsx improved
Ready to say: "This order matters. We did not build isolated pages randomly. The team first built the shell, then data, then detail pages, then watchlist, then polish and fixes."
Course

Course Coverage — Module 1 + Module 2 Mapped To CineVault

Module 1 concepts used

HTML/CSS layout
Semantic structure, responsive layout, flex/grid thinking, reusable visual patterns.
M1 W1
JS basics
Variables, conditionals, template literals, string/number conversions in route and API data.
M1 W2
Functions + arrays
map, filter, find, helpers, early returns, edge-case handling.
M1 W3
Objects/classes mindset
Data shape thinking: movie object, TV object, watchlist entry object, backend entry id.
M1 W4
DOM thinking
Events and UI updates moved from manual DOM to React state-driven rendering.
M1 W5

Module 2 concepts used

Components
Pages and reusable cards/heroes split the app into readable parts.
M2
Props
Parent pages pass media data and callback actions into child components.
M2
State/hooks
useState and useEffect handle loading, filters, fetches, and route-driven reloads.
M2
Routing
React Router connects browser URLs to specific pages and detail IDs.
M2
API calls
Axios calls TMDB and the watchlist backend, using environment variables.
M2
Advanced

What Went Beyond The Course

Team engineering

  • Feature branches and pull-request style merging.
  • Conflict resolution after multiple people changed shared files.
  • Commit graph review for accountability and explanation.

App architecture

  • Two API layers: TMDB data and watchlist persistence.
  • Context provider as shared data controller.
  • Helpers that normalize movie and TV object shapes.

Production habits

  • Environment variables for API URLs and keys.
  • Deployment files and Render/Vercel style config.
  • Defensive loading/error paths so pages do not crash.
Ready to say: "The course gave us the parts. The beyond-course work was connecting those parts like a real app: routes, APIs, state, backend persistence, branches, and deployment."
3 Speakers

Easy Speaking Split For Three Teammates

Ali says

"I will cover the shared interface and watchlist flow: routing shell, sidebar, heroes, movie/TV pages, and the context that lets the whole app save titles consistently."
App.jsxWatchlistContext.jsxHeroBanner.jsx

Noah says

"I will cover the data engine: initial React setup, TMDB API flow, movie and TV detail sections, search, and how shared movie state powers the UI."
MovieProvider.jsxMovieDetail.jsxSearch.jsx

Almas says

"I will cover genre and profile work: genre browsing, profile/dashboard components, footer work, loading bugs, and the fixes that made routes safer."
Genrepage.jsxProfile.jsxMoodBoard.jsx
Contributor

Ali Saad — Interface Architecture + Watchlist Flow

Ali Saad GitHub avatar
Ali Saad
@aliihsaad
Interface architecture, watchlist flow, Search UI improvements

Commit evidence

  • sidebar: navigation layout and route access.
  • hero-banner: hero component, movie detail hero, TV detail route.
  • watchlist: watchlist context, helper files, watchlist page, add/remove/update flow.
  • tv-episodes-ui: episode cards, season selector, video dialog, episode styles.
  • profile-dashboard: recommendation and profile dashboard components.
  • search-ui-improvements: Search.css and Search.jsx visual/UX polish, merged as PR #16.

Beginner explanation

Ali's work is the part users touch first: navigation, page layout, hero sections, and watchlist actions. The important method is state shared through context, so a save button in a detail page can update the Watchlist page without manually passing props through every component.
Ready to say: "My section connects UI clicks to shared app state. I focused on making the user journey smooth: browse, open detail, add to watchlist, then manage it later."
Code Spec

Ali Code Spec — Watchlist Action Pattern

What the method does

→ Page receives movie or TV item.
→ Page calls addToWatchlist(item, type).
→ Context checks if that item is already saved.
→ API sends POST /watchlist only for new entries.
→ State updates so UI re-renders.

Commented explanation

// Beginner mental model: one function protects the app from duplicate saves.
async function addToWatchlist(item, mediaType) {
  // 1. Check current state first.
  if (findSavedTitle(watchlist, item.id, mediaType)) return

  // 2. Build one clean backend object from messy TMDB data.
  const entry = buildWatchlistEntry(item, mediaType)

  // 3. Save it, then update React state so the UI changes.
  const saved = await watchlistApi.post("/watchlist", entry)
  setWatchlist((current) => [...current, saved.data])
}
Code Walkthrough

Ali Code — Sidebar Routes And Navigation Shell

Method explained

Pattern: store navigation links as data, then render them with .map(). This avoids rewriting the same JSX for every route and keeps the sidebar consistent.
→ Link object stores path, label, and icon.
→ NavLink handles active route styling.
→ Sidebar remains outside page content.
→ Only the route page changes when the URL changes.

Commented code spec

// SideBar.jsx - presenter version of the method
const links = [
  { to: "/", label: "Home", icon: Home },
  { to: "/movies", label: "Movies", icon: Film },
  { to: "/tv-shows", label: "TV Shows", icon: Tv },
  { to: "/watchlist", label: "Watchlist", icon: Bookmark }
]

// map turns each object into one visible navigation link.
{links.map(({ to, label, icon: Icon }) => (
  <NavLink to={to}>
    <Icon /> {label}
  </NavLink>
))}
PR #16

Ali Code — Search UI Improvements

Evidence

  • Branch: search-ui-improvements
  • Merge: PR #16 into origin/main
  • Commit: 30d06bd authored by Ali
  • Files: src/pages/Search.css, src/pages/Search.jsx
  • Change size: 318 insertions, 35 deletions

Commented code spec

// Search.jsx - presenter version of the UI state flow
const [query, setQuery] = useState("")
const [results, setResults] = useState([])
const [loading, setLoading] = useState(false)

async function handleSearch(event) {
  event.preventDefault()
  // Empty input should not trigger a useless API request.
  if (!query.trim()) return

  setLoading(true)
  const data = await searchMovies(query)
  setResults(data)
  setLoading(false)
}

// Search.css handles the improved visual layout and result states.
Contributor

Noah — Data Foundation, Detail Pages, Search

Noah Perez GitHub avatar
Noah Perez
@NoahPerez
Data foundation, TMDB flow, detail pages, search feature

Commit evidence

  • initial setup: Vite app, packages, axios, router, env example.
  • MovieContext: shared TMDB movie state and homepage data.
  • details-page: cast, gallery, reviews, overview for movie and TV details.
  • episodes-list: context refactor and episode feature integration.
  • search: Search page, filter/button components, comments.
  • deployment: Vercel config and deployment checks.

Beginner explanation

Noah's work is the data pipeline. The important method is fetch data once, store it in state/context, then render many components from that state. That is why details, search, and lists can reuse the same API mental model.
Ready to say: "My section explains how raw TMDB data becomes usable UI: request data, handle loading, store the result, and render reusable cards and sections."
Code Spec

Noah Code Spec — Route ID To Detail Page

What the method does

→ User clicks a movie/TV card.
→ React Router changes URL to /movie/:id or /tv/:id.
→ Page reads the id with useParams().
→ useEffect() fetches detail data when id changes.
→ Detail components render cast, gallery, reviews, and overview.

Commented explanation

// Beginner mental model: the URL decides what data to load.
const { id } = useParams()
const [detail, setDetail] = useState(null)

useEffect(() => {
  let cancelled = false

  async function loadDetail() {
    // Fetch only the current media id.
    const data = await api.getDetail(id)
    if (!cancelled) setDetail(data)
  }

  loadDetail()
  return () => { cancelled = true }
}, [id])
Code Walkthrough

Noah Code — Movie Context As The Data Engine

Method explained

Pattern: keep API data in a provider so pages can read movie lists, loading states, and helper methods without fetching separately in every component.
→ Provider loads shared movie data.
→ State stores popular movies, TV shows, genres, and loading flags.
→ Pages call useMovies().
→ Components receive stable data through props.

Commented code spec

// MovieProvider.jsx - presenter version of shared movie state
export function MovieProvider({ children }) {
  const [movies, setMovies] = useState([])
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    // Load shared TMDB data once when the provider starts.
    getPopularMovies()
      .then(setMovies)
      .finally(() => setLoading(false))
  }, [])

  return (
    <MovieContext.Provider value={{ movies, loading }}>
      {children}
    </MovieContext.Provider>
  )
}
Code Walkthrough

Noah Code — Detail Sections: Cast, Gallery, Reviews

Method explained

Pattern: a detail page should not become one huge file. Fetch the detail data on the page, then pass smaller data sets into focused carousel/section components.
CastCarousel.jsx GalleryCarousel.jsx ReviewsCarousel.jsx

Commented code spec

// MovieDetail.jsx - presenter version of detail composition
<InfoOverview media={movie} />

// Each section receives only the data it needs.
<CastCarousel cast={credits.cast} />
<GalleryCarousel images={images.backdrops} />
<ReviewsCarousel reviews={reviews.results} />

// Beginner meaning:
// MovieDetail controls the page data.
// Child components control one visual section each.
Contributor

Almas — Genre Discovery, Profile, Bug Fixing

Almas Khan GitHub avatar
Almas Khan
@Almas-Eclipse
Genre discovery, profile dashboard, loading and route-safety fixes

Commit evidence

  • GenrePage: genre route/page and genre component structure.
  • Genre and Footer: footer, Genres, carousel UI, homepage integration.
  • Profile Page: ProfileHeader, MoodBoard, StatsRow, profile page.
  • Loading bug fix: profile and genre loading behavior corrections.
  • Console cleanup: removed debug logging from API file.

Beginner explanation

Almas's work is user-facing correctness. The important method is defensive rendering: before showing data, check if the data exists, if the route id is valid, and if loading is finished. That prevents blank screens and wrong genre results.
Ready to say: "My section explains the discovery and profile features, and also the small fixes that make a project stable during a live presentation."
Code Spec

Almas Code Spec — Route Safety For Genres

What the method does

→ User clicks a genre.
→ URL carries a genre id.
→ Page converts route param from string to number.
→ Page filters only matching movie/TV items.
→ Loading and empty states prevent broken UI.

Commented explanation

// Beginner mental model: route params arrive as strings.
const { genreId } = useParams()
const selectedGenreId = Number(genreId)

// Guard: avoid filtering with an invalid id.
if (Number.isNaN(selectedGenreId)) {
  return <p>Genre not found</p>
}

// Filter only items that include the selected genre id.
const matches = movies.filter((movie) =>
  movie.genre_ids?.includes(selectedGenreId)
)
Code Walkthrough

Almas Code — Profile Dashboard Components

Method explained

Pattern: split profile UI into smaller components so each part has a clear job: header, stats, mood board, and recommendations.
ProfileHeader.jsx StatsRow.jsx MoodBoard.jsx Profile.jsx

Commented code spec

// Profile.jsx - presenter version of profile composition
const savedCount = watchlist.length
const watchedCount = watchlist.filter((item) => item.watched).length

// Profile stays readable because each section has its own component.
<ProfileHeader user={profile} />
<StatsRow saved={savedCount} watched={watchedCount} />
<MoodBoard items={watchlist} />

// Beginner meaning: profile data changes when watchlist state changes.
Bug Fix

Almas Code — Loading Bug And Safe Rendering

Method explained

Pattern: never run list operations or render dependent UI before the data exists. Loading and empty states are not decoration; they prevent runtime errors and wrong screens.
→ Check loading first.
→ Check empty data second.
→ Render the real component only when data is ready.

Commented code spec

// Defensive rendering pattern used for profile / genre fixes.
if (loading) {
  return <Loading />
}

if (!items || items.length === 0) {
  return <p>No results yet</p>
}

// Only map after the array exists.
return items.map((item) => (
  <MovieCard key={item.id} movie={item} />
))
Teamwork

Integration Lessons From Week 6 Project Work

Merge conflict risk

  • Shared files like App.jsx, Homepage.jsx, and context files changed often.
  • Conflicts happened because multiple features needed the same routes and providers.
  • Resolution required checking which code belonged in which architecture layer.

Branch discipline

  • Feature branches kept large work isolated.
  • Pulling latest main before adding more code mattered.
  • Stashing or committing local work protected unfinished code.

Beginner lesson

  • Git manages code versions and branches.
  • NPM runs scripts and installs packages.
  • Confusing them is common early on, but the team corrected the workflow.
Ready to say: "The project taught us more than React. It taught us how teams coordinate: branch, pull, merge, resolve, test, and explain."
Overview

What The Team Built

🗺 Routing & Layout

Set up the entire URL structure and sidebar navigation — how the app knows which page to show for each URL.
App.jsx main.jsx SideBar.jsx

🎬 Hero Components

Two reusable hero sections — one for browsing pages (rotating carousel), one for detail pages (actions bar).
HeroBanner.jsx MovieDetailHero.jsx

📺 TV Shows Feature

Full TV browsing + detail pages added from scratch. Reuses the same hero & card components as movies.
TVShows.jsx TVDetail.jsx

📋 Watchlist System (CRUD)

Full Create / Read / Update / Delete watchlist connected to a real backend server. Add, watch, unwatch, remove.
WatchlistContext.jsx watchlistHelpers.js

🏠 Home + Movies Split

Separated the landing page from the movies browser. Home = cinematic intro. Movies = browse & add.
Homepage.jsx Movies.jsx

🔧 Backend API Server

Set up the JSON Server that persists watchlist data. Deployed to Render with CORS enabled for cross-origin calls.
cinevault-api app.js
Roadmap

How We’ll Explain The Work

1
Start with the app shell
First we explain the route system, sidebar, and providers because they are the foundation every page depends on.
2
Then the reusable UI
Next we show how HeroBanner, MovieDetailHero, MovieCard, and MovieCarousel avoid duplicated UI for movies and TV shows.
3
Then the data flow
After the UI, we explain how TMDB data, watchlist context, helper functions, and the backend communicate.
4
End with decisions + fixes
Finally we highlight bug fixes, advanced patterns, verification steps, and exactly which files each teammate changed.
Presentation story: “We did not just add screens. We connected routing, reusable components, shared state, backend CRUD, and safer data handling into one working feature flow.”
Guide

How To Read The Code Slides

Instead of reading every line like a textbook, we explain each code block using three questions: what does this code control, why did we need it, and what problem does it prevent?
1 · What it controlsExample: routing controls which page appears, context controls shared watchlist state, and helper functions control how movie/TV data is normalized.
2 · Why it existsExample: we used reusable components so Movies and TV Shows could share the same UI instead of duplicating similar JSX.
3 · What bug it preventsExample: loading guards prevent a false “not found” message before the API request finishes.
How to speak
“This part receives data, this part decides what to render, and this part updates state after the backend confirms the change.”
How teammates can follow
Each code slide has a plain-English explanation beside it, so the team can understand the decision even if the syntax is new.
Takeaway: the slides now explain the code like a walkthrough, not like a file dump.
File

App.jsx — The App's URL Map

src/App.jsx ✓ In Curriculum
What it does: Lists every URL the app can show, and maps each one to a page component. React Router reads this list and renders the right page when the URL changes — without a full browser reload.
// The app layout: sidebar always visible,
// page content changes based on the URL
function App() {
  return (
    <div className="app-layout">
      <Sidebar />           // always rendered
      <main>
        <Routes>
          <Route path="/"         element={<Homepage />} />
          <Route path="/movies"   element={<Movies />} />
          <Route path="/tv-shows" element={<TVShows />} />
          <Route path="/movie/:id" element={<MovieDetails />} />
          <Route path="/tv/:id"    element={<TVDetail />} />
          <Route path="/watchlist" element={<Watchlist />} />
          <Route path="*"          element={<NotFound />} />
        </Routes>
      </main>
    </div>
  )
}
src/main.jsx ✓ In Curriculum
What it does: The very first file that runs. It starts React and wraps the app with providers so every page can access shared data (movies + watchlist).
createRoot(document.getElementById('root')).render(
  <StrictMode>
    <BrowserRouter>        // enables URL routing
      <MovieProvider>      // TMDB data everywhere
        <WatchlistProvider> // watchlist everywhere
          <App />
        </WatchlistProvider>
      </MovieProvider>
    </BrowserRouter>
  </StrictMode>
)
  • :id is dynamic — it changes for every movie or TV show
  • * is a catch-all — any unknown URL shows the NotFound page
  • Wrapping with providers means all pages are inside the providers and can access their data
Routing

Routing Walkthrough — What Each Piece Means

main.jsxApp.jsx
Plain English: this is the app shell. The providers give data to the whole app, the sidebar stays fixed, and the route list decides which page appears inside the main area.
createRoot(root).render(
  <BrowserRouter>
    <MovieProvider>
      <WatchlistProvider>
        <App />
      </WatchlistProvider>
    </MovieProvider>
  </BrowserRouter>
)

// App.jsx
<Sidebar />
<main>
  <Routes>
    <Route path="/movie/:id" element={<MovieDetails />} />
    <Route path="/tv/:id" element={<TVDetail />} />
    <Route path="*" element={<NotFound />} />
  </Routes>
</main>

Line-by-line explanation

BrowserRouter
Enables routing. It watches the browser URL and lets React swap page components without reloading the whole website.
MovieProvider
Shares TMDB data. Pages can call useMovies() instead of passing movie data through many components manually.
WatchlistProvider
Shares watchlist state and actions. Any page can add, remove, or update saved titles.
Sidebar outside Routes
Keeps navigation visible. The sidebar does not remount when the page route changes.
/movie/:id
Dynamic route. The :id part changes for every movie and is read using useParams().
*
Catch-all route. Any unknown URL goes to the NotFound page instead of showing a blank screen.
Flow

End-to-End App Flow — From URL to Screen

Plain English: React does not load a brand new HTML page for every URL. Instead, React Router checks the current URL and swaps the page component inside the existing layout.
1 · Browser URL
User visits /movies, /tv/1399, or /watchlist.
→
2 · BrowserRouter
Reads the URL and enables React Router navigation without full page reloads.
→
3 · App.jsx
Finds the matching <Route> and chooses which page component should appear.
→
4 · Page
The page calls hooks like useMovies() or useWatchlist().
→
5 · Components
Reusable components display the final UI: hero, cards, carousel, buttons.
What to notice: the sidebar stays visible because it is outside <Routes>. Only the content inside <main> changes when the URL changes.
File

SideBar.jsx — Navigation with Active States

src/components/SideBar.jsx ✓ In Curriculum
What it does: The navigation bar visible on every screen. Links automatically highlight when you're on that page — no manual logic needed.
// All nav links in one array
// To add a new link: just add one object here
const links = [
  { to: "/movies",    label: "Movies",   icon: Film     },
  { to: "/tv-shows",  label: "TV Shows", icon: Tv       },
  { to: "/search",    label: "Search",   icon: Search   },
  { to: "/watchlist", label: "Watchlist",icon: Bookmark },
]

// .map() builds a NavLink for each item
{links.map((link) => (
  <NavLink
    to={link.to}
    className={({ isActive }) =>
      isActive ? "sidebar__link--active" : ""
    }
  >
    <link.icon size={18} />
    <span>{link.label}</span>
  </NavLink>
))}

Concepts Used

NavLink
Like <Link> but knows which URL is active. Passes isActive so we can apply a CSS class automatically.
Curriculum
.map()
Loops over the links array and creates one NavLink per item — no copy-pasting the same JSX block 4 times.
Curriculum
Lucide icons
Each icon is a React component: <Film />, <Tv />. Import from lucide-react library.
Library
Object array pattern
Storing links as objects ({ to, label, icon }) instead of hardcoding JSX — makes the list easy to modify.
Curriculum
Why NavLink instead of Link? <Link> just navigates. <NavLink> also tells you if that link is the current page (isActive), so we can highlight it without writing any extra logic.
Navigation

Sidebar Walkthrough — Data-Driven Navigation

The key decision: each menu item is stored as data, then .map() renders the JSX. That makes the sidebar easier to maintain because adding a new route means adding one object.
const links = [
  { to: "/movies", label: "Movies", icon: Film },
  { to: "/tv-shows", label: "TV Shows", icon: Tv },
]

{links.map((link) => {
  const Icon = link.icon

  return (
    <NavLink
      key={link.to}
      to={link.to}
      className={({ isActive }) =>
        isActive ? "sidebar__link--active" : ""
      }
    >
      <Icon size={18} />
      <span>{link.label}</span>
    </NavLink>
  )
})}

What each part is doing

links array
Single source of truth. The route, label, and icon for the sidebar live together in one place.
.map()
Turns data into UI. It creates one NavLink for each object in the array.
key={link.to}
Helps React track list items. The URL is unique, so it works well as the key.
Icon = link.icon
Dynamic component rendering. The icon component is stored in the object and rendered like <Icon />.
isActive
Automatic active state. NavLink tells us whether this link matches the current URL, so CSS can highlight it.
Takeaway: the sidebar is reusable because the JSX structure stays the same while the data controls what appears.
File

HeroBanner.jsx — Reusable Browse Hero

src/components/HeroBanner.jsx ✓ In Curriculum
What it does: The big featured-item section on the Movies and TV Shows pages. One component works for both because it accepts a movie OR a TV show as a prop — it handles both data shapes.
// Both movie and TV show objects are accepted
// They have different field names — we handle both:
const title    = movie.title || movie.name || "Untitled"
const released = movie.release_date || movie.first_air_date

// Detect if it's a TV show to build the right link
const isTv     = Boolean(movie.name && !movie.title)
const link     = isTv ? `/tv/${movie.id}` : `/movie/${movie.id}`

// Backdrop image builds the CSS background
const bgUrl = `https://image.tmdb.org/t/p/original${movie.backdrop_path}`

Add to Watchlist — 4 Button States

The button shows different text based on what's happening. This uses useState + async/await — both curriculum concepts.
const [status, setStatus] = useState("idle")

const handleAdd = async () => {
  setStatus("saving")        // → "Saving..."
  try {
    const result = await onAdd(movie)
    setStatus(
      result?.alreadySaved ? "already-saved" : "idle"
    )                          // → "Already Saved" or "+ Add"
  } catch {
    setStatus("error")        // → "Try Again"
  }
}
Why does onAdd come from the parent? So HeroBanner stays reusable. The Movies page passes a movie add function; the TV Shows page passes a TV add function. Same component, different behaviour.
Pattern

Reusable Component Pattern — Parent Owns Logic, Child Owns UI

Parent page responsibility

  • Fetches or receives data from context.
  • Knows the media type: movie or TV.
  • Passes callbacks like onAdd or onSelectFeature.
  • Controls state, such as the active hero index.
// Movies.jsx knows this is a movie
<HeroBanner
  movie={featuredMovie}
  onAdd={(movie) => addToWatchlist(movie, "movie")}
/>

Child component responsibility

  • Displays the UI using props.
  • Does not need to know the whole app.
  • Calls the callback when the user clicks.
  • Stays reusable because the parent decides behaviour.
// HeroBanner.jsx does not import the watchlist context
const handleAdd = () => {
  if (onAdd) onAdd(movie)
}
Why this matters: the same HeroBanner can work on Movies and TV Shows because it receives behaviour from the parent instead of hardcoding one use case.
Files

Homepage.jsx + Movies.jsx — Two Separate Jobs

src/pages/Homepage.jsx ✓ In Curriculum
What it does: The landing page at /. Shows a cinematic background that slowly rotates between movie backdrops. The title stays fixed, so the page feels like a brand intro with clear entry points into the rest of the app.
// Only movies with a backdrop image are used
const backdropMovies = useMemo(() =>
  popularMovies.filter((m) => m.backdrop_path).slice(0, 8)
, [popularMovies])

// Background image rotates every 9 seconds
useEffect(() => {
  const timer = window.setInterval(() => {
    setIndex((i) => (i + 1) % backdropMovies.length)
    // % = modulo: loops back to 0 after the last image
  }, 9000)

  return () => window.clearInterval(timer)
  // ↑ cleanup: cancels the timer when page unmounts
}, [backdropMovies.length])
src/pages/Movies.jsx ✓ In Curriculum
What it does: The actual movie browser at /movies. Has the rotating HeroBanner + carousels of movie posters. Before the split, / was doing both jobs — one page doing too much.
// Get data + functions from context
const { popularMovies, getPopularMovies } = useMovies()
const { addToWatchlist } = useWatchlist()

// Fetch once when the page loads
useEffect(() => { getPopularMovies() }, [])

// Only movies with backdrop go into the hero
const heroMovies = useMemo(() =>
  popularMovies.filter((m) => m.backdrop_path)
, [popularMovies])

// Hero auto-rotates every 5 seconds
const [heroIndex, setHeroIndex] = useState(0)
const featured = heroMovies[heroIndex % heroMovies.length]
Before the split: / acted like a movie browser. Now / is a clean welcome screen and /movies is where you actually browse. Single responsibility.
Files

TVShows.jsx + TVDetail.jsx — TV Feature

src/pages/TVShows.jsx ✓ In Curriculum
What it does: TV browsing page. Reuses HeroBanner and MovieCarousel even though the data is TV shows — just passes mediaType="tv" so cards link to /tv/:id instead of /movie/:id.
// Same hero component as Movies —
// but onAdd passes "tv" as the media type
<HeroBanner
  movie={featuredShow}
  featuredMovies={heroShows.slice(0, 5)}
  onAdd={(show) => addToWatchlist(show, "tv")}
/>

// Same carousel — but mediaType tells it to
// link to /tv/:id instead of /movie/:id
<MovieCarousel
  movies={popularTvShows}
  mediaType="tv"
/>
src/pages/TVDetail.jsx ✓ In Curriculum Bug Fix Pattern
The bug that was fixed: Clicking "More Info" would sometimes flash "TV show not found" for a second before data loaded. The problem: the component checked for data before the fetch finished. Fix: track which ID has finished loading.
const { id } = useParams() // e.g. "12345" from /tv/12345

// requestedId = id that has FINISHED loading
const [requestedId, setRequestedId] = useState(null)

useEffect(() => {
  let cancelled = false

  getTvDetails(id).finally(() => {
    if (!cancelled) setRequestedId(id) // mark done
  })

  return () => { cancelled = true } // ← safety cleanup
}, [id])

// Only render the page when BOTH are ready
if (loading || requestedId !== id) {
  return <Loading />
}
Why cancelled? If the user navigates away before the fetch finishes, we don't want React to update state on an unmounted component — that causes a warning/crash.
Detail Pages

Detail Page Walkthrough — URL ID, API Request, Safe Rendering

MovieDetail.jsxTVDetail.jsxLoading Guard
The bug we had to avoid: the detail page can render before the API data arrives. If the page checks the selected item too early, it may show “not found” even though the data is still loading.
const { id } = useParams()

useEffect(() => {
  getTvDetails(id)
}, [id])

if (loading || requestedId !== id) {
  return <Loading message="Loading TV show details..." />
}

if (!selectedTv || String(selectedTv.id) !== String(id)) {
  return <p>TV show not found.</p>
}

What each guard protects

useParams()
Reads the dynamic URL. In /tv/1399, the value 1399 becomes the id.
[id]
Refetches when URL changes. If the user moves from one detail page to another, the effect runs again for the new item.
loading
Blocks early rendering. The page stays on the loading component while data is still being fetched.
requestedId !== id
Confirms the current request finished. This avoids mixing old data with the new URL.
String(...)
Compares safely. URL params are strings, while API IDs can be numbers, so both are converted before comparison.
Takeaway: the detail page has to handle four states: loading, error, not found, and ready.
Concept

Movie vs TV Data Shape — Why Fallbacks Were Needed

TMDB does not use one universal shape

MeaningMovie fieldTV fieldFallback
Titletitlenamemovie.title || movie.name
Release daterelease_datefirst_air_daterelease_date || first_air_date
Lengthruntimenumber_of_seasonsruntime for movies, seasons for TV
Detail route/movie/:id/tv/:idchoose route from media type

The beginner-friendly reason

A component like HeroBanner receives an object. That object could be a movie or a TV show. If the component only reads movie.title, TV shows would appear as blank because TV shows use name.
// Safe display name
const title = item.title || item.name || "Untitled"

// Safe date
const date = item.release_date || item.first_air_date || ""
Team decision: handle the difference once inside reusable components/helpers, instead of forcing every page to repeat the same checks.
File

MovieDetailHero.jsx — Detail Page Hero

src/components/MovieDetailHero.jsx ✓ In Curriculum
What it does: The large hero section on detail pages — backdrop image, title, genres, and action buttons. One component handles both MovieDetail and TVDetail by accepting a mediaType prop.
export default function MovieDetailHero({
  movie,               // the movie or TV show object
  mediaType = "movie",  // "movie" or "tv"
  savedEntry,          // null = not in watchlist
  onAdd,               // add to watchlist
  onToggleWatched,     // mark watched/unwatched
  onRemove,            // remove from watchlist
})

// Shows seasons for TV, runtime for movies
const runtimeText = mediaType === "tv"
  ? `${movie.number_of_seasons} seasons`
  : `${movie.runtime} min`

Smart Buttons — 2 States Based on savedEntry

If savedEntry is null → show "Add to Watchlist". If it exists → show watchlist management buttons. This is conditional rendering with a ternary — curriculum.
{savedEntry ? (
  // Already saved → show watchlist controls
  <>
    <button onClick={onToggleWatched}>
      {savedEntry.watched ? "Mark Unwatched" : "Mark Watched"}
    </button>

    <button onClick={handleRemove}>
      {removeStatus === "removing"
        ? "Removing..."
        : "Remove"}
    </button>
  </>
) : (
  // Not saved → show add button
  <button onClick={handleAdd}>+ Add to Watchlist</button>
)}
  • savedEntry = null → "Add to Watchlist"
  • savedEntry.watched = false → "Mark Watched"
  • savedEntry.watched = true → "Mark Unwatched"
Files

watchlist.js + cinevault-api/app.js — The Backend

src/api/watchlist.js ✓ In Curriculum
What it does: Creates a dedicated Axios instance that always points at the watchlist backend. This keeps TMDB calls and watchlist calls completely separate — different base URLs, clean code.
import axios from "axios"

// A pre-configured Axios instance
// Every call automatically goes to the right server
const watchlistApi = axios.create({
  baseURL: import.meta.env.VITE_WATCHLIST_API_URL
  // reads from the .env file — not hardcoded
})

export default watchlistApi
The .env file (each teammate needs this):
VITE_WATCHLIST_API_URL=https://cinevault-api-2vhd.onrender.com
cinevault-api repo app.js ✓ In Curriculum
What it does: The backend server. JSON Server reads db.json and exposes a full REST API automatically — no database setup, no routes to write by hand.
const router = jsonServer.router("db.json")
// ↑ db.json becomes the database
// Auto-creates GET/POST/PUT/DELETE /watchlist

// CORS middleware — allows the frontend
// to call this server from a different domain
server.use((req, res, next) => {
  res.header("Access-Control-Allow-Origin", "*")
  res.header("Access-Control-Allow-Methods",
    "GET, POST, PUT, PATCH, DELETE")
  next()
})

// Health check — lets us verify the server is up
server.get("/health", (req, res) =>
  res.json({ status: "ok" })
)
server.listen(PORT, "0.0.0.0")
Flow

Backend Request Lifecycle — What Happens When Data Is Saved

User click
User presses + Add, Mark Watched, or Remove.
→
Context method
addToWatchlist, updateWatchlistEntry, or removeFromWatchlist runs.
→
API layer
watchlistApi sends Axios request to the Render backend URL.
→
JSON Server
The backend updates db.json through REST routes.
→
React state
After success, local watchlist state is updated so the UI changes instantly.
Why this API file matters: pages and context do not need to know the full backend URL. They just call watchlistApi.get(), .post(), .put(), or .delete().
Why the backend is separate: the frontend handles UI, while the backend stores watchlist data. This separation makes the app closer to a real full-stack project.
API

Watchlist API Contract — What The Frontend Sends

REST actions used by the watchlist

ActionEndpointUsed when
GET/watchlistLoad all saved titles when the app starts or the watchlist page opens.
POST/watchlistCreate a new saved movie or TV show.
PUT/watchlist/:idUpdate an existing backend entry, like toggling watched/unwatched.
DELETE/watchlist/:idRemove one saved entry from the backend.
Important: :id here is the backend entry ID, not the TMDB movie/TV ID.

Payload shape saved to JSON Server

{
  tmdbId: 1399,
  mediaType: "tv",
  title: "Game of Thrones",
  posterPath: "/poster.jpg",
  backdropPath: "/backdrop.jpg",
  releaseDate: "2011-04-17",
  rating: 8.4,
  watched: false
}
tmdbId
Used for links back to /movie/:id or /tv/:id.
mediaType
Tells the app whether this saved item is a movie or a TV show.
watched
Stores the user’s watched/unwatched status and powers the filter tabs.
Files

WatchlistContext — Shared State for the Whole App

context/watchlistStore.js context/useWatchlist.js context/WatchlistContext.jsx ✓ In Curriculum
Why 3 files? React's Fast Refresh requires that component files only export components. The Provider is a component. The context object and the hook are not. Splitting them avoids warnings.
// watchlistStore.js — creates the context
export const WatchlistContext = createContext(null)

// useWatchlist.js — the hook pages import
export function useWatchlist() {
  const ctx = useContext(WatchlistContext)
  if (!ctx) throw new Error("Must be inside WatchlistProvider")
  return ctx
}

// WatchlistContext.jsx — the provider component
export function WatchlistProvider({ children }) {
  const [watchlist,          setWatchlist]          = useState([])
  const [isWatchlistLoading, setIsWatchlistLoading] = useState(true)
  ...
  return <WatchlistContext.Provider value={value}>
    {children}
  </WatchlistContext.Provider>
}

CRUD Methods

// CREATE
const addToWatchlist = useCallback(async (item, type) => {
  if (findSavedTitle(watchlist, item.id, type))
    return { alreadySaved: true }  // no duplicates
  const res = await watchlistApi.post("/watchlist", newEntry)
  setWatchlist((cur) => [...cur, res.data])
}, [watchlist])

// UPDATE
const updateWatchlistEntry = useCallback(async (id, fields) => {
  const res = await watchlistApi.put(`/watchlist/${id}`, {
    ...currentEntry, ...fields  // merge changes
  })
  setWatchlist((cur) => replaceSavedTitle(cur, res.data))
}, [watchlist])

// DELETE
const removeFromWatchlist = useCallback(async (id) => {
  await watchlistApi.delete(`/watchlist/${id}`)
  setWatchlist((cur) => removeSavedTitle(cur, id))
}, [])
Context

WatchlistContext Walkthrough — Add, Update, Remove

WatchlistContext.jsxwatchlistHelpers.js
This is the central logic: the context checks if the title is already saved, builds a clean backend payload, waits for the backend, then updates local React state.
const addToWatchlist = async (item, mediaType) => {
  const existing = findSavedTitle(
    watchlist, item.id, mediaType
  )

  if (existing) {
    return { alreadySaved: true, entry: existing }
  }

  const payload = buildWatchlistEntry(item, mediaType)
  const created = await createWatchlistEntry(payload)

  setWatchlist((current) => [...current, created])
  return { entry: created }
}

Why this method is safe

findSavedTitle
Prevents duplicates. The same movie or TV show should not be saved twice.
buildWatchlistEntry
Normalizes the data. Movies and TV shows have different TMDB fields, but the backend receives one clean shape.
await create...
Backend first. The app saves to the server before updating the local list.
setWatchlist(current => ...)
Functional state update. This uses the latest state value, which is safer when state changes depend on previous state.
return object
Gives feedback to UI. The button can show “Already Saved” or “Saved” based on the result.
Presenter line: “Context keeps the business logic in one place so the UI components stay cleaner.”
Why

Why Watchlist Logic Belongs In Context

The problem without context

Many pages need the same saved-title information: Movies, TV Shows, Movie Detail, TV Detail, and Watchlist. Without context, the same data and functions would be passed down through many layers of props.
// Prop drilling problem
<App watchlist={watchlist}>
  <Movies watchlist={watchlist}>
    <HeroBanner watchlist={watchlist} />
  </Movies>
</App>

The context solution

The provider stores the shared watchlist state once. Any child page can call useWatchlist() and get the same data and actions.
// Cleaner: page asks the provider directly
const {
  watchlist,
  addToWatchlist,
  updateWatchlistEntry,
  removeFromWatchlist
} = useWatchlist()
Key explanation: Context became the “watchlist control center.” Pages focus on rendering, while the provider handles loading, saving, updating, deleting, duplicate checks, and shared counts.
File

watchlistHelpers.js — Clean Helper Functions

src/context/watchlistHelpers.js ✓ In Curriculum
What it does: Small, pure functions that do one job each. They handle the differences between movie and TV show data shapes so the context file stays clean and readable.
// Movies have .title, TV shows have .name
export function getTitle(item) {
  return item?.title || item?.name || "Untitled"
}

// Check BOTH tmdbId AND mediaType
// (a movie and a TV show can share the same numeric ID!)
export function findSavedTitle(watchlist, tmdbId, type) {
  return watchlist.find((item) =>
    String(item.tmdbId) === String(tmdbId) &&
    getMediaType(item)  === type
  ) || null
}
// String() cast: URL params are strings ("123"),
// TMDB IDs are numbers (123) — cast both to compare safely

// Movies use .release_date, TV shows use .first_air_date
export function getReleaseDate(item) {
  return item?.release_date || item?.first_air_date || ""
}

buildWatchlistEntry — Normalises TMDB Data

Problem: A movie object and a TV object from TMDB look different. We need one consistent format to save to the backend. This function converts either into a single shape.
export function buildWatchlistEntry(item, typeOverride) {
  return {
    mediaType:    getMediaType(item, typeOverride),
    tmdbId:       item.id,
    title:        getTitle(item),         // .title or .name
    overview:     item.overview,
    poster_path:  item.poster_path,
    release_date: getReleaseDate(item),  // release or air date
    vote_average: item.vote_average,
    watched:      false,
    addedAt:      new Date().toISOString().split("T")[0],
  }
}
  • removeSavedTitle() — uses .filter() to return the list minus the deleted entry
  • replaceSavedTitle() — uses .map() to swap one updated entry in the list
Helpers

Helper Functions Walkthrough — Normalizing Movie and TV Data

Why helpers matter: they keep repeated “movie vs TV” checks out of the components. Instead of writing fallback logic everywhere, components call one clean function.
export const getTitle = (item) =>
  item?.title ?? item?.name ?? "Untitled"

export const getReleaseDate = (item) =>
  item?.release_date ?? item?.first_air_date ?? ""

export const findSavedTitle = (list, tmdbId, mediaType) =>
  list.find((entry) =>
    String(entry.tmdbId) === String(tmdbId) &&
    entry.mediaType === mediaType
  )

What this protects

title ?? name
Movie/TV compatibility. Movies use title, TV shows use name.
release_date ?? first_air_date
Date compatibility. The UI can show the year without caring which media type it is.
?.
Crash prevention. If the API data is missing for a moment, optional chaining returns undefined instead of crashing.
??
Safer fallback. It only falls back when the value is null or undefined, not for every falsy value.
String IDs
Reliable matching. Backend values and URL/API values can have different types, so converting avoids false mismatches.
Takeaway: helpers make the rest of the project easier to read because the tricky data rules live in one place.
File

Watchlist.jsx — Your Saved Titles

src/pages/Watchlist.jsx ✓ In Curriculum
What it does: Shows all saved titles. Has filter tabs by type (Movies / TV Shows) and status (Watched / Unwatched). Each card links to the correct detail page.
// All data comes from context
const { watchlist, watchlistCounts } = useWatchlist()

// Filtering with .filter() — curriculum
const filteredTitles = watchlist.filter((title) => {
  if (filter === "movie")    return getMediaType(title) === "movie"
  if (filter === "tv")       return getMediaType(title) === "tv"
  if (filter === "watched")  return title.watched === true
  if (filter === "unwatched")return !title.watched
  return true  // "all"
})

Important: Two Different IDs

This was an easy mistake to make — the watchlist entry has two IDs and they mean different things:
// title.id     → the backend's own ID (for CRUD calls)
// title.tmdbId → the real TMDB movie/TV ID (for links)

// Using title.id for links would go to the wrong page!
const detailPath = mediaType === "tv"
  ? `/tv/${title.tmdbId}`   // ✓ correct
  : `/movie/${title.tmdbId}`

<Link to={detailPath}><MovieCard movie={title} /></Link>
Filter tabs are split into two groups: Media type (All / Movies / TV Shows) and status (Watched / Unwatched). Separating them makes the UI easier to scan — two different questions, two different rows.
Example

User Journey — Adding A TV Show To The Watchlist

What the user sees

1
Open TV Shows. The page loads popular TV data and displays a rotating hero.
2
Click + Add. The button changes to a saving state so the user gets feedback.
3
Open Watchlist. The saved TV show appears with the correct title, poster, and media type.
4
Click the card. The card links to /tv/:tmdbId, not the backend ID.

What the code does

// TVShows.jsx
onAdd={(show) => addToWatchlist(show, "tv")}

// WatchlistContext.jsx
const newEntry = buildWatchlistEntry(show, "tv")
const res = await watchlistApi.post("/watchlist", newEntry)
setWatchlist((current) => [...current, res.data])

// Watchlist.jsx
const detailPath = `/tv/${title.tmdbId}`
Important: this example proves the full stack is working: UI click → Context → API → Backend → db.json → React state → Watchlist page.
Advanced Hooks

useMemo & useCallback — Performance Helpers

↑ Beyond Curriculum useMemo
What is it?
useMemo caches the result of a calculation. React re-runs a component every time state changes. Without useMemo, expensive calculations (like filtering 20 movies) run again on every single render — even if the movies list didn't change.
Simple example
// Without useMemo — runs EVERY render
const heroMovies = popularMovies.filter((m) => m.backdrop_path)

// With useMemo — only runs when popularMovies changes
const heroMovies = useMemo(
  () => popularMovies.filter((m) => m.backdrop_path),
  [popularMovies]  // ← dependency: only recalculate when this changes
)
Used in: Homepage, Movies, TVShows (hero movie list), WatchlistContext (watchlist counts).
↑ Beyond Curriculum useCallback
What is it?
useCallback caches a function. In JavaScript, every time a component re-renders, functions defined inside it are recreated as brand new objects. This can cause child components to re-render unnecessarily. useCallback keeps the same function reference unless its dependencies change.
Simple example
// Without useCallback — new function on every render
const removeFromWatchlist = async (id) => { ... }

// With useCallback — same function reference
// (only changes when dependencies change)
const removeFromWatchlist = useCallback(
  async (id) => { ... },
  []  // ← no dependencies = never recreated
)
Used in: All 4 CRUD methods in WatchlistContext + loadWatchlist + findWatchlistEntry.
Modern JS Syntax

Optional Chaining & Nullish Coalescing

↑ Beyond Curriculum ?. — Optional Chaining
What is it?
Safely reads a property even if the object is null or undefined. Without it, accessing a property on null crashes the app. With ?., it just returns undefined instead of throwing an error.
Before vs After
// Before — crashes if item is null
return item.title

// After — safe, returns undefined if item is null
return item?.title

// Chain multiple levels safely
return item?.details?.poster_path
Used in: watchlistHelpers.js — every getter function uses ?. because TMDB data might be incomplete on first load.
↑ Beyond Curriculum ?? — Nullish Coalescing
What is it?
A smarter fallback than ||. The || operator falls back if the value is falsy (includes 0, empty string ""). The ?? operator only falls back if the value is null or undefined. This matters when 0 or "" are valid values.
The difference
// With || — 0 triggers the fallback (wrong!)
const count = 0 || "no items"  // → "no items" ← BUG

// With ?? — only null/undefined triggers fallback
const count = 0 ?? "no items"  // → 0 ← correct

// Used in MovieCard for size variants:
const cls = sizeClasses[size] ?? sizeClasses.md
// If size prop is invalid → fall back to medium
Used in: MovieCard.jsx for size variant lookup, helper functions for safe fallbacks.
Async Patterns

Promise.finally() & The Cancel Guard

↑ Beyond Curriculum .finally()
What is it?
Runs a block of code after a promise — whether it succeeded or failed. Useful for cleanup that should always happen, like turning off a loading spinner. You've used try/catch in curriculum — .finally() is an addition to that pattern.
Comparison
// Without finally — repeat the setLoading(false) twice
try {
  await fetchData()
  setLoading(false)  // ← repeated
} catch {
  setLoading(false)  // ← repeated again
}

// With finally — runs once no matter what
try {
  await fetchData()
} catch {
  setError("Failed")
} finally {
  setLoading(false)  // ← runs always
}
Used in: TVDetail.jsx to mark that the requested ID has finished loading, WatchlistContext to always set loading to false.
↑ Beyond Curriculum Cancel Guard Pattern
What is it?
Prevents updating state after the component is gone. Imagine: a user clicks "More Info" then immediately goes back. The fetch is still running. When it finishes, it tries to call setState on a component that no longer exists — React throws a warning.
The pattern
useEffect(() => {
  let cancelled = false  // flag starts false

  fetchSomething().then((data) => {
    if (!cancelled) setData(data)  // only update if still mounted
  })

  return () => {
    cancelled = true  // ← cleanup: flip the flag when unmounting
  }
}, [])
Used in: TVDetail.jsx — the let cancelled = false inside the useEffect that fetches the TV show details.
Bugs + Fixes

Challenges We Hit — And How We Resolved Them

Development challenges

Async loading bugs
Detail, profile, and genre pages could show loading too long, show stale data, or fail when API data arrived late.
Fixed
Watchlist sync
The UI state and JSON Server backend had to stay aligned after create, update, and delete actions.
CRUD
Movie vs TV shapes
TMDB movie and TV objects use different field names like title vs name.
API
Search UX
Search needed clear inputs, filters, empty states, responsive cards, and visual polish after it became functional.
UI
Team UI consistency
Three developers touched different screens, so shared components and a common visual direction were needed.
Team
Deployment/env setup
The frontend needed correct API keys, deployed URLs, and CORS-safe backend communication.
Deploy

How we handled bugs

  • Reproduce: confirm the bug in the browser instead of guessing.
  • Find the source: check whether the issue is state, props, routing, API data, or CSS.
  • Fix small: change the smallest part that explains the bug.
  • Retest the flow: navigate, refresh, add/remove items, and check loading/empty states.
  • Merge carefully: keep feature branches focused so the team can review and integrate faster.
Ready to say: "The hardest parts were not only building features. The real challenge was making them stable: loading states, API timing, watchlist sync, route changes, and keeping UI consistent across three people."
Ali's Reflection

Ali — Challenges, Bugs, And Lessons Learned

Ali Saad GitHub avatar
Ali Saad
@aliihsaad
Interface architecture, watchlist CRUD, routing flow, Search UI improvements

Main challenges

1
Connecting UI clicks to real data changes

The watchlist was not just a button. It had to create, read, update, and delete items while keeping the UI and backend in sync.

2
Avoiding duplicated watchlist items

Movie and TV items can look similar, so the logic needed to compare both the TMDB id and the media type before saving.

3
Routing and layout integration

The sidebar and detail routes had to feel connected across the app, so navigation, active links, and page structure needed to stay consistent.

4
Search UI polish after functionality

The Search page worked first, but making it clear, responsive, and visually consistent required a second UI pass and className/CSS cleanup.

5
Rotating hero banner logic

The hero banner needed to rotate featured movies automatically without breaking the page. The method was useState for the current slide, useEffect for timing, setInterval for auto-rotation, modulo % to loop back, and clearInterval cleanup.

Lessons learned

State sync is harder than it looks

A feature feels simple from the UI, but behind it there is backend data, local React state, duplicate checks, loading feedback, and user expectations.

AI helped most after the logic worked

For styling, the best workflow was: build the component first, use Noah's visual direction, ask AI for className planning, audit the hooks, then generate CSS.

Rotating hero banner method

useState stores which hero item is active. useEffect starts the timer. setInterval moves to the next item. Modulo % loops from the last item back to the first. Cleanup with clearInterval prevents timer bugs.

// Hero banner: rotate automatically and loop
useEffect(() => {
  const timer = setInterval(() => {
    setCurrentIndex((current) =>
      (current + 1) % heroItems.length
    );
  }, 5000);

  return () => clearInterval(timer);
}, [heroItems.length]);

// Duplicate guard idea used in watchlist logic
const alreadySaved = watchlist.some((saved) =>
  saved.tmdbId === item.id &&
  saved.mediaType === mediaType
);

if (alreadySaved) return;
Ready to say: "My biggest challenge was making UI actions become real application behavior. A button click had to talk to shared state, avoid duplicates, update the backend, and still feel instant to the user. I also had to handle timed hero-banner rotation without creating timer bugs."
Noah's Reflection

Noah — What Was Challenging

Noah Perez GitHub avatar
Noah Perez
@NoahPerez
Data foundation, detail pages, search, and reusable UI sections

Main challenges

1
Breaking pages into smaller components

The challenge was deciding where one component should end and the next should begin, instead of keeping everything inside one huge page file.

2
Choosing where logic belongs

Some logic belongs in Context API because many components need it. Other logic should stay inside the page because only that page uses it.

3
Reviewing AI-generated code

AI could generate useful ideas, but the real work was reading the code, understanding what it changed, and making sure it fit CineVault.

4
Review ratings and stars

TMDB review ratings are numeric data, but the UI needed readable star ratings, so the data had to be converted into a visual format.

How to explain it simply

Component split

A page is like a full movie scene. Components are the smaller reusable parts inside it: cards, carousels, review rows, rating stars, and detail sections.

Context vs page logic

If many pages need the data, it belongs in Context. If only one page needs it, keeping it inside that page makes the code easier to understand.

// TMDB rating example: number -> stars
const ratingOutOfFive = Math.round(tmdbRating / 2);
const stars = "★".repeat(ratingOutOfFive) +
  "☆".repeat(5 - ratingOutOfFive);
Ready to say: "My biggest challenge was not only making the detail/search pages work. It was learning how to split the UI into reusable pieces, decide where logic belongs, and review AI-generated code until I actually understood it."
Almas's Reflection

Almas — Bugs Fixed And Lessons Learned

Almas Khan GitHub avatar
Almas Khan
@Almas-Eclipse
Genre discovery, profile dashboard, loading fixes, and conflict resolution

Bugs I fixed

1
Genre page stuck loading forever

GenrePage.jsx was using the shared loading state from Context. The genre helper did not reset that loading state, so the page could stay stuck.

Fix: remove the shared loading dependency for this page and use a local data check like if (!movies.length).

2
Genre movies not loading on click

Clicking a genre from MoodBoard showed no movies because useParams() returns the route id as a string, like "28", but genreMovies was keyed by number, like 28.

Fix: convert the route id with Number(id) before using it to read genre data.

Big mistakes and lessons

Merge conflicts

Early in the project, shared files like Homepage.jsx caused major merge conflicts because multiple teammates changed the same area. The fix was manual conflict resolution and understanding both versions before keeping the final code.

Use of AI

Almas used Claude AI for debugging errors, understanding why bugs happened, styling components to match the design, and navigating Git commands like stashing, resolving conflicts, and managing branches.

// Route params are strings by default
const { id } = useParams(); // "28"

// Convert before using numeric genre keys
const genreId = Number(id);
const movies = genreMovies[genreId] || [];

if (!movies.length) {
  return <Loading />;
}
Ready to say: "My biggest lesson was that bugs are often small details: a shared loading state, a string instead of a number, or a merge conflict in a shared file. Fixing them forced me to understand the code instead of only writing new features."
Bug Deep Dive

Bug Example — Loading And Stale API Data

The problem

  • Users could navigate faster than the TMDB request finished.
  • A page could unmount while the API request was still running.
  • Late responses could try to update state after the page changed.
  • The result was wrong content, stuck loading, or unstable UI behavior.

Beginner explanation

React state updates are asynchronous. If an API call finishes after the user leaves the page, we need a guard so the old request does not update the new screen.

The fix pattern

// Simplified pattern used for API-driven pages
useEffect(() => {
  let cancelled = false;
  setLoading(true);

  fetchDetails(id)
    .then((data) => {
      if (cancelled) return;
      setDetails(data);
    })
    .catch((error) => {
      if (!cancelled) setError(error);
    })
    .finally(() => {
      if (!cancelled) setLoading(false);
    });

  return () => { cancelled = true; };
}, [id]);
Resolution: use loading flags, safe fallbacks, and cleanup guards so old requests cannot damage the current UI.
Bug Deep Dive

Bug Example — Watchlist State And Backend Sync

The problem

  • The save button exists inside movie/TV detail pages.
  • The saved items are displayed later on the Watchlist page.
  • The backend keeps the permanent copy, but React state controls the instant UI.
  • If we updated only one side, the app could show duplicates or stale items.
Ready to say: "The watchlist bug was a data-sync problem. We had to make the backend and React state agree after every action."

The resolution

async function addToWatchlist(item) {
  const exists = watchlist.some((saved) =>
    saved.tmdbId === item.id &&
    saved.mediaType === item.media_type
  );

  if (exists) return; // avoid duplicate saves

  const savedItem = await createWatchlistItem(normalizeItem(item));
  setWatchlist((current) => [...current, savedItem]);
}
Important method: save to the backend first, then update React state with functional setState so the UI reflects the confirmed saved item.
UI Library

Beyond The Curriculum — shadcn/ui

Beyond CurriculumUI Componentscomponents.json

Where we used it

  • Search: Button and Input from src/components/ui.
  • Movie cards: reusable Badge component.
  • Episodes: reusable Dialog for video previews.
  • Carousels: shared carousel component for movies, cast, gallery, reviews, and genres.
  • Styling foundation: Tailwind, class-variance-authority, tailwind-merge, and the local cn() helper.

Why it matters

// Examples from the project structure
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import {
  Carousel,
  CarouselContent,
  CarouselItem
} from "@/components/ui/carousel";

Beginner explanation

shadcn/ui is beyond the Ironhack curriculum. It gives reusable component files inside our project, so we can import them, customize them, and keep the UI consistent.

Ready to say: "We also used shadcn/ui beyond the curriculum. It helped us avoid rebuilding common UI pieces like buttons, inputs, badges, dialogs, and carousels from zero."
AI Usage

How We Used AI — Debugging, Research, and Learning Methods

↑ Beyond Curriculum Learning Workflow
This is worth mentioning: we did not use AI as a copy-paste shortcut. We used it like a debugging partner: first to understand the problem, then to research possible methods, then to explain why a method works, and finally to help verify the implementation.
1
We reproduced the issue. We checked what broke in the browser and what the app was supposed to do.
2
We described the error to AI. We included the failing behavior, the related file, and the expected behavior.
3
AI suggested possible methods. For example: loading guards, async cleanup, finally(), optional chaining, or safer fallback logic.
4
We asked for the method explanation. We wanted to know what the method does, when to use it, and what each part of the code means.
5
We adapted and tested it ourselves. We changed the code to match CineVault, then checked the UI, build, and behavior.
How to explain it to the team: AI helped us learn faster, but we still had to decide whether the suggestion fit the project, edit the real files, understand the tradeoff, and test the result.
TVDetail.jsx Async useEffect Promise.finally()
Concrete example
Problem: clicking “More Info” on a TV show could show a not-found message before the API request finished. The page was checking selectedTv too early.
Method AI helped me research
Loading guard + requested ID tracking. The idea is to keep the page in loading mode until the request for the current URL ID has actually finished. This prevents the UI from showing a false “not found” state while data is still loading.
useEffect(() => {
  let cancelled = false
  setRequestedId(null)

  getTvDetails(id).finally(() => {
    if (!cancelled) setRequestedId(id)
  })

  return () => { cancelled = true }
}, [id, getTvDetails])

if (loading || requestedId !== id) {
  return <Loading message="Loading TV show details..." />
}
What this method does: finally() runs after success or failure, requestedId confirms the current request finished, and the cancel guard avoids updating state after leaving the page.
UI Styling Workflow

AI Usage — UI And Styling

Beyond Curriculum AI Assisted Styling Noah Visual Reference

How We Used AI For UI

The component or page was built functionally first. AI was used after the logic worked, mainly to translate the existing visual direction into clean classNames and CSS.

1
Build the feature first

The page/component works with basic styling before AI styling starts.

2
Give AI the visual direction

We used Noah's early app visuals as the design reference so the result stayed close to the CineVault style.

3
Share the JSX file

AI was given the page/component JSX so it could understand the real structure, not invent fake markup.

4
Ask for a styling plan

Before CSS, we asked for better classNames and a plan to match the general app style.

5
Audit the classNames

After applying classNames, AI checked whether every needed element had a usable class hook.

6
Generate and import CSS

Only then did AI generate the full CSS file, which we imported into the JSX page or component.

Ready to say: "AI helped us with styling, but not by randomly generating the UI. We already had working components and Noah's visual direction. We used AI like a UI assistant: plan the classNames, check the JSX hooks, generate CSS, then import it into the component."

Why This Was A Controlled Workflow

What AI improved

Class naming, layout structure, spacing, visual consistency, responsive styling, and writing complete CSS faster.

What the team still owned

The React logic, component behavior, data flow, design direction, testing in the browser, and deciding what CSS actually stayed in the project.

// Step 1: functional JSX gets intentional classNames
import "./Search.css";

function Search() {
  return (
    <section className="search-page">
      <form className="search-panel">
        <input className="search-input" />
        <button className="search-submit">Search</button>
      </form>
      <div className="search-results-grid">
        {movies.map(movie => (
          <MovieCard key={movie.id} movie={movie} />
        ))}
      </div>
    </section>
  );
}

// Step 2: AI generates CSS only after the JSX hooks are clear
.search-page { padding: 2rem; }
.search-panel { display: flex; gap: 1rem; }
.search-results-grid { display: grid; }

Presentation framing: this is beyond-course usage because it combines React implementation, AI prompting, UI direction, and CSS generation into one repeatable workflow.

AI Usage

AI-Assisted Methods — What We Researched and How We Used Them

This is the honest framing: AI helped us find and understand methods beyond the curriculum, but we still had to choose the correct pattern, adapt it to CineVault, and verify it in the browser.
MethodWhy we researched itHow we used it in CineVaultWhat it prevents / improves
.finally()We needed code that runs after an async request whether it succeeds or fails.Used in async detail/watchlist logic to finish loading or mark the request as completed.Stops duplicated cleanup code and keeps loading states consistent.
Cancel guardWe needed to avoid updating state after leaving a page during an async request.Used a cancelled boolean inside useEffect cleanup.Prevents state updates on an unmounted component and avoids stale UI behavior.
Requested ID guardThe TV detail page could show not-found before the correct request finished.Stored the ID that finished loading and compared it with the current URL ID.Prevents false not-found screens when navigating quickly or waiting for data.
?. optional chainingTMDB data can be missing or temporarily undefined during loading.Used in helper functions and display logic when reading nested fields.Prevents crashes like “Cannot read properties of undefined.”
?? nullish fallbackWe needed safer fallbacks than || for API values.Used when choosing movie/TV title, date, poster, rating, and fallback values.Protects valid values like 0 or empty strings from being replaced accidentally.
Functional setStateWatchlist updates depend on the previous list.Used setWatchlist(current => ...) after create/update/delete.Uses the latest state and avoids bugs from stale state values.
What we would say: “AI helped us understand which method fits which bug. The final work was still our responsibility: we applied the method to our files, tested the behavior, and kept the code consistent with the project.”
Architecture

Full Watchlist CRUD Flow — How It All Connects

➕ CREATE — Add Title

→ Click + Add to Watchlist
→ HeroBanner calls onAdd(movie)
→ Page calls addToWatchlist(item, "movie")
→ Context checks findSavedTitle()
→ New? → POST /watchlist
→ State: [...current, newEntry]
→ Button text changes instantly

✏️ UPDATE — Toggle Watched

→ Click Mark Watched
→ Detail page calls onToggleWatched()
→ Context calls updateWatchlistEntry(id, {watched: !current})
→ PUT /watchlist/:id
→ replaceSavedTitle() swaps entry
→ Button → "Mark Unwatched"

🗑 DELETE — Remove Title

→ Click Remove
→ Detail page calls onRemove()
→ Context calls removeFromWatchlist(savedEntry.id)
→ DELETE /watchlist/:id
→ removeSavedTitle() filters state
→ savedEntry = null → "+ Add" returns
READ = loadWatchlist() → GET /watchlist → fills watchlist state → Watchlist page renders all saved titles.
Key insight: The backend is the source of truth. The React state is just a local mirror. Every CRUD operation hits the backend first, then updates the mirror — so the UI is always in sync with what's actually saved.
Reflection

Curriculum vs. Beyond — Full Overview

✓ Applied from Ironhack Curriculum

JSON Server
Course-covered backend mock API tool: turns a JSON file into REST endpoints for CRUD practice.
Course Tool
React components
Every JSX file — composing the whole UI from small reusable pieces.
Core
Props
Passing data and callbacks from parent to child — HeroBanner, MovieDetailHero, MovieCarousel, MovieCard.
Core
useState
Hero index, filter tab, button status, loading flags — state drives every dynamic piece.
Core
useEffect
Fetch data on mount, run timers with cleanup, react to URL param changes.
Core
Context API
createContext, useContext, Provider pattern — both Movie and Watchlist global state.
Core
React Router v6
Routes, Route, Link, NavLink, useParams — the whole URL system.
Core
Axios
GET, POST, PUT, DELETE — all TMDB calls and all watchlist API calls.
Core
async / await
Every API call — try/catch/finally error handling pattern.
Core
.filter() .map() .find()
Filter watchlist, render card lists, find saved entries — used everywhere.
Core
Ternary / conditional render
Switching button states, loading states, empty states — all ternary operators.
Core
Spread operator
{...current, ...updatedFields} — merging watchlist entry updates.
Core
Template literals
Building TMDB image URLs, API paths like `/watchlist/${id}`.
Core

↑ Applied Beyond Curriculum

AI-assisted research
Used AI to debug errors, compare possible methods, explain beyond-curriculum patterns, then adapt and test the final code.
Workflow
useMemo
Cache filtered arrays (hero movies, watchlist counts) to avoid recalculating every render.
Perf
useCallback
Keep CRUD function references stable in Context so child components don't re-render needlessly.
Perf
optional chaining (?.)
Safe property access on possibly-null API data — item?.title instead of crashing.
ES2020
nullish coalescing (??)
Smarter fallback than || — only triggers on null/undefined, not on 0 or "".
ES2020
Promise.finally()
Runs cleanup (e.g. stop loading spinner) regardless of success or error.
Async
Cancel guard pattern
let cancelled = false in useEffect — prevents state updates on unmounted components.
Pattern
Functional setState
setWatchlist((current) => [...current, newItem]) — uses the previous state, avoids stale closures.
Pattern
shadcn/ui
Additional UI component library used beyond the curriculum for reusable buttons, inputs, badges, dialogs, and carousels.
Beyond UI Library
CORS middleware
HTTP headers that allow the frontend (different domain) to call the backend safely.
Backend
Vite env variables
import.meta.env.VITE_* — keeps API URLs out of source code, different per machine.
Tool
Testing

How We Verified The Work

Manual app checks

  • Open /, /movies, /tv-shows, and /watchlist.
  • Confirm sidebar active states change correctly.
  • Click movie and TV cards and verify the correct detail route.
  • Refresh detail pages directly to make sure URL params still work.

Watchlist CRUD checks

  • Add a movie and confirm it appears in Watchlist.
  • Add a TV show and confirm it links to /tv/:id.
  • Mark watched, then mark unwatched.
  • Remove an entry and confirm it disappears after backend update.

Developer checks

# Build the project
npm run build

# Focused lint check
npx eslint src/App.jsx \
  src/components/HeroBanner.jsx \
  src/pages/TVShows.jsx \
  src/pages/TVDetail.jsx
Meaning: build checks whether Vite can compile the app. Lint checks whether touched files follow the code rules.
Summary

Team File Map — Quick Reference

🗺 Routing & Setup

FileJob
src/main.jsxApp entry + providers
src/App.jsxAll routes + layout

🧭 Navigation

components/SideBar.jsxSidebar + active links
components/SideBar.cssSidebar styles

🧩 UI Components

components/HeroBanner.jsxBrowse hero
components/HeroBanner.css
components/MovieDetailHero.jsxDetail page hero
components/MovieDetailHero.css
components/MovieCard.jsxPoster card
components/MovieCarousel.jsxScrollable row
components/Loading.jsxLoading spinner UI

📄 Pages

FileRoute
pages/Homepage.jsx/
pages/Movies.jsx/movies
pages/MovieDetail.jsx/movie/:id
pages/TVShows.jsx/tv-shows
pages/TVDetail.jsx/tv/:id
pages/Watchlist.jsx/watchlist
pages/NotFound.jsx*

🖼 Assets

assets/NotFound.svg404 illustration
assets/loading.svgSpinner animation

🗄 Context / State

FileJob
context/WatchlistContext.jsxProvider + CRUD
context/watchlistStore.jscreateContext()
context/useWatchlist.jsCustom hook
context/watchlistHelpers.jsHelper functions

🌐 API Layer

api/watchlist.jsAxios instance

🖥 Backend (separate repo)

cinevault-api/app.jsJSON Server + CORS
cinevault-api/db.jsonWatchlist storage
Total: ~25 files created or significantly updated across 2 repos.