import { useEffect, useRef, useState } from “react”;
import StarRating from “./StarRating”;
import { useMovies } from “./useMovies”;
import { useLocalStorageState } from “./useLocalStorageState”;
import { useKey } from “./useKey”;
const average = (arr) =>
arr.reduce((acc, cur, i, arr) => acc + cur / arr.length, 0);
const KEY = “1caea674”;
// stateful component – because we needed to pass movies down to the MovieList component
export default function App() {
const [query, setQuery] = useState(“”);
const [selectedId, setSelectedId] = useState(null);
// handleCloseMovie can be used before it is defined because it is hoisted
// it woud not be hoisted if handleCloseMovie uses an => function!
const { movies, isLoading, error } = useMovies(query /*handleCloseMovie*/);
const [watched, setWatched] = useLocalStorageState([], “watched”);
//const [watched, setWatched] = useState([]);
// the callback function must be a pure function and cannot
// receive any arguments and must return a value
// Also, the callback is only executed on the initial render
/*const [watched, setWatched] = useState(function () {
const storedWatched = localStorage.getItem(“watched”);
return JSON.parse(storedWatched);
});*/
function handleSelectMovie(id) {
setSelectedId((selectedId) => (id === selectedId ? null : id));
}
function handleCloseMovie() {
setSelectedId(null);
}
function handleAddWatched(movie) {
setWatched((watched) => […watched, movie]);
// have to add the movie to the list before we add it to
// local storage because setWatched() is asynchronous so
// watched is stale and we have to add the movie to watched
// because watched hasn’t been updated yet.
//localStorage.setItem(“watched”, JSON.stringify([…watched, movie]));
}
function handleDeleteWatched(id) {
setWatched((watched) => watched.filter((movie) => movie.imdbID !== id));
}
return (
<>
<NavBar movies={movies}>
<Search query={query} setQuery={setQuery} />
<NumResults movies={movies} />
</NavBar>
<Main>
<Box>
{/*{isLoading ? <Loader /> : <MovieList movies={movies} />}</Box>*/}
{isLoading && <Loader />}
{!isLoading && !error && (
<MovieList movies={movies} onSelectMovie={handleSelectMovie} />
)}
{error && <ErrorMessage message={error} />}
</Box>
<Box>
{selectedId ? (
<MovieDetails
selectedId={selectedId}
onCloseMovie={handleCloseMovie}
onAddWatched={handleAddWatched}
watched={watched}
/>
) : (
<>
<WatchedSummary watched={watched} />
<WatchedMovieList
watched={watched}
onDeleteWatched={handleDeleteWatched}
/>
</>
)}
</Box>
</Main>
</>
);
}
function Loader() {
return <p className=“loader”>Loading…</p>;
}
function ErrorMessage({ message }) {
return (
<p className=“error”>
<span>❌</span> {message}
</p>
);
}
// structural component – only responsible for the layout of the app
function NavBar({ children }) {
return (
<nav className=“nav-bar”>
<Logo />
{children}
</nav>
);
}
// presentational component – no state
function Logo() {
return (
<div className=“logo”>
<span role=“img”>🍿</span>
<h1>usepopcorn</h1>
</div>
);
}
// stateful component
function Search({ query, setQuery }) {
/*
// on load we want the search element to have focus
useEffect(function () {
const el = document.querySelector(“.search”);
el.focus();
}, []);
*/
// lets replace calling document.querySelector call (above) with a useRef hook
const inputEl = useRef(null);
useKey(“Enter”, function () {
if (document.activeElement === inputEl.current) return;
inputEl.current.focus();
setQuery(“”);
});
return (
<input
className=“search”
type=“text”
placeholder=“Search movies…”
value={query}
onChange={(e) => setQuery(e.target.value)}
ref={inputEl}
/>
);
}
// presentational component – no state
function NumResults({ movies }) {
return (
<p className=“num-results”>
Found <strong>{movies.length}</strong> results
</p>
);
}
// structural component – only responsible for the layout of the app
function Main({ children }) {
return <main className=“main”>{children}</main>;
}
function Box({ children }) {
const [isOpen, setIsOpen] = useState(true);
return (
<div className=“box”>
<button className=“btn-toggle” onClick={() => setIsOpen((open) => !open)}>
{isOpen ? “–” : “+”}
</button>
{isOpen && children}
</div>
);
}
/*
// stateful component
function ListBox({ children }) {
const [isOpen1, setIsOpen1] = useState(true);
return (
<div className=”box”>
<button
className=”btn-toggle”
onClick={() => setIsOpen1((open) => !open)}
>
{isOpen1 ? “–” : “+”}
</button>
{isOpen1 && children}
</div>
);
}
// stateful component
function WatchedBox() {
const [watched, setWatched] = useState(tempWatchedData);
const [isOpen2, setIsOpen2] = useState(true);
return (
<div className=”box”>
<button
className=”btn-toggle”
onClick={() => setIsOpen2((open) => !open)}
>
{isOpen2 ? “–” : “+”}
</button>
{isOpen2 && (
<>
<WatchedSummary watched={watched} />
<WatchedMovieList watched={watched} />
</>
)}
</div>
);
}
*/
// presentation component – no state
function MovieList({ movies, onSelectMovie }) {
return (
<ul className=“list list-movies”>
{movies?.map((movie) => (
<Movie movie={movie} onSelectMovie={onSelectMovie} key={movie.imdbID} />
))}
</ul>
);
}
// presentational component – no state
function Movie({ movie, onSelectMovie }) {
return (
<li onClick={() => onSelectMovie(movie.imdbID)} key={movie.imdbID}>
<img src={movie.Poster} alt={`${movie.Title} poster`} />
<h3>{movie.Title}</h3>
<div>
<p>
<span>🗓</span>
<span>{movie.Year}</span>
</p>
</div>
</li>
);
}
function MovieDetails({ selectedId, onCloseMovie, onAddWatched, watched }) {
const [movie, setMovie] = useState({});
const [isLoading, setIsLoading] = useState(false);
const [userRating, setUserRating] = useState(“”);
const countRef = useRef(0);
useEffect(
function () {
if (userRating) countRef.current++;
},
[userRating]
);
const isWatched = watched.map((movie) => movie.imdbID).includes(selectedId);
const watchedUserRating = watched.find(
(movie) => movie.imdbID === selectedId
)?.userRating;
const {
Title: title,
Year: year,
Poster: poster,
Runtime: runtime,
imdbRating,
Plot: plot,
Released: released,
Actors: actors,
Director: director,
Genre: genre,
} = movie;
function handleAdd() {
const newWatchedMovie = {
imdbID: selectedId,
title,
year,
poster,
imdbRating: Number(imdbRating),
runtime: Number(runtime.split(” “).at(0)),
userRating,
countRatingDecisions: countRef.current,
};
onAddWatched(newWatchedMovie);
onCloseMovie();
}
useKey(“Escape”, onCloseMovie);
useEffect(
function () {
async function getMovieDetails() {
setIsLoading(true);
const res = await fetch(
`http://www.omdbapi.com/?apikey=${KEY}&i=${selectedId}`
);
const data = await res.json();
setMovie(data);
setIsLoading(false);
}
getMovieDetails();
},
[selectedId]
);
useEffect(
function () {
if (!title) return;
document.title = `Movie | ${title}`;
// a cleanup function once the component is unmounted
return function () {
document.title = “Rueben’s Movie Finder”;
};
},
[title]
);
return (
<div className=“details”>
{isLoading ? (
<Loader />
) : (
<>
<header>
<button className=“btn-back” onClick={onCloseMovie}>
←
</button>
<img src={poster} alt={`Poster of ${movie} movie`} />
<div className=“details-overview”>
<h2>{title}</h2>
<p>
{released} • {runtime}
</p>
<p>{genre}</p>
<p>
<span>⭐</span>
{imdbRating} IMDB rating
</p>
</div>
</header>
<section>
<div className=“rating”>
{!isWatched ? (
<>
<StarRating
maxRating={10}
size={24}
onSetRating={setUserRating}
/>
{userRating > 0 && (
<button className=“btn-add” onClick={handleAdd}>
+ Add to watched list
</button>
)}
</>
) : (
<p>
You rated this movie {watchedUserRating}
<span>⭐</span>
</p>
)}
</div>
<p>
<em>{plot}</em>
</p>
<p>Starring: {actors}</p>
<p>Directed by: {director}</p>
</section>
</>
)}
</div>
);
}
// presentational component – no state
function WatchedSummary({ watched }) {
const avgImdbRating = average(watched.map((movie) => movie.imdbRating));
const avgUserRating = average(watched.map((movie) => movie.userRating));
const avgRuntime = average(watched.map((movie) => movie.runtime));
return (
<div className=“summary”>
<h2>Movies you watched</h2>
<div>
<p>
<span>#️⃣</span>
<span>{watched.length} movies</span>
</p>
<p>
<span>⭐️</span>
<span>{avgImdbRating.toFixed(2)}</span>
</p>
<p>
<span>🌟</span>
<span>{avgUserRating.toFixed(2)}</span>
</p>
<p>
<span>⏳</span>
<span>{avgRuntime.toFixed(0)} min</span>
</p>
</div>
</div>
);
}
// presentational component – no state
function WatchedMovieList({ watched, onDeleteWatched }) {
return (
<ul className=“list”>
{watched.map((movie) => (
<WatchedMovie
movie={movie}
key={movie.imdbID}
onDeleteWatched={onDeleteWatched}
/>
))}
</ul>
);
}
// presentational component – no state
function WatchedMovie({ movie, onDeleteWatched }) {
return (
<li key={movie.imdbID}>
<img src={movie.poster} alt={`${movie.title} poster`} />
<h3>{movie.title}</h3>
<div>
<p>
<span>⭐️</span>
<span>{movie.imdbRating}</span>
</p>
<p>
<span>🌟</span>
<span>{movie.userRating}</span>
</p>
<p>
<span>⏳</span>
<span>{movie.runtime} min</span>
</p>
<button
className=“btn-delete”
onClick={() => onDeleteWatched(movie.imdbID)}
>
X
</button>
</div>
</li>
);
}