React keywords: fetch, localStorage, useEffect, useRef, useState

Project overview:

Project shows: The concept of prop drilling is used for the first time as props are passed from parent to child, and then from child to child. Component composition is also used for the first time with the use of the children property. The React useEffect() event handler is also used to process side effect, such as changes to state, or when the App mounts. The userRef hook is used to create a reference to the DOM input element <input … ref={inputEl} /> and then uses the hook useKey to determine if the DOM element was selected when the ‘enter’ key was pressed:

useKey(“Enter”, function () {
    if (document.activeElement === inputEl.current) return;
    inputEl.current.focus();
    setQuery(“”);
  });

– The parent App() retrieves a list of movies from an API ‘const { movies, isLoading, error } = useMovies(query /*handleCloseMovie*/);’ and then passes down (prop drilling) this list to the child component <MovieList movies={movies} … />’.

– The App() passes 2 child components to the NavBar() child component using component composition. Here is the JSX:

‘<NavBar movies={movies}>
        <Search query={query} setQuery={setQuery} />  – becomes part of the children prop
        <NumResults movies={movies} />  – becomes part of the children prop
</NavBar>’

And the NavBar() accepts the <Search…/> and <NumResults…/> components as children:

‘function NavBar({ children }) {
return (
<nav className=”nav-bar”>
<Logo />
{children}
</nav>
);
}’

– The useEffect() event handler can be called when the App mounts (renders the very first time) by including an empty [], or empty effect, such as:

useEffect(function() {

…}, [] – means only call this effect when the App mounts.

)

– The useEffect can also be called by a prop value change, such as inside the function:

‘function MovieDetails({ selectedId, … }) {

useEffect(function () { – is called whenever the selectedId is changed
async function getMovieDetails() { …
const res = await fetch(`http://www.omdbapi.com/?apikey=${KEY}&i=${selectedId}`);

}
getMovieDetails(); – notice that the defined function inside the effect MUST be called!
},
[selectedId]
);

}

– The useLocalStorageState function uses localStorage.setItem(…)/getItem(…) methods to keep track of the watched movies.

Code:

<!DOCTYPE html>
<html lang=“en”>
  <head>
    <meta charset=“utf-8” />
    <link rel=“icon” href=“%PUBLIC_URL%/favicon.ico” />
    <meta name=“viewport” content=“width=device-width, initial-scale=1” />
    <meta name=“theme-color” content=“#000000” />
    <meta
      name=“description”
      content=“Web site created using create-react-app”
    />
    <link rel=“apple-touch-icon” href=“%PUBLIC_URL%/logo192.png” />
    <!–
      manifest.json provides metadata used when your web app is installed on a
      user’s mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
    –>
    <link rel=“manifest” href=“%PUBLIC_URL%/manifest.json” />
    <!–
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.

 

      Unlike “/favicon.ico” or “favicon.ico”, “%PUBLIC_URL%/favicon.ico” will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    –>
    <title>usepopcorn</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id=“root”></div>
    <!–
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.

 

      You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.

 

      To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    –>
  </body>
</html>
import React, { useState } from “react”;
import ReactDOM from “react-dom/client”;
import “./index.css”;
import App from “./App”;

//import StarRating from “./StarRating”;

/*function Test() {
  const [movieRating, setMovieRating] = useState(0);

  return (
    <div>
      <StarRating color=”blue” maxRating={10} onSetRating={setMovieRating} />
      <p>This movie was rated {movieRating} stars</p>
    </div>
  );
}*/

const root = ReactDOM.createRoot(document.getElementById(“root”));
root.render(
  <React.StrictMode>
    <App />
    {/*<StarRating
      maxRating={5}
      messages={[“Terrible”, “Bad”, “Okay”, “Good”, “Amazing”]}
    />
    <StarRating size={24} color=”red” className=”test” defaultRating={3} />
    <Test />*/}
  </React.StrictMode>
);
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}>
              &larr;
            </button>
            <img src={poster} alt={`Poster of ${movie} movie`} />
            <div className=“details-overview”>
              <h2>{title}</h2>
              <p>
                {released} &bull; {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>
  );
}
import { useState } from “react”;
import PropTypes from “prop-types”;

const containerStyle = {
  display: “flex”,
  alignItems: “center”,
  gap: “16px”,
};

const starContainerStyle = {
  display: “flex”,
};

StarRating.prototype = {
  maxRating: PropTypes.number,
  defaultRating: PropTypes.number,
  color: PropTypes.string,
  size: PropTypes.number,
  messages: PropTypes.array,
  className: PropTypes.string,
  onSetRating: PropTypes.func,
};

export default function StarRating({
  maxRating = 5,
  color = “#fcc419”,
  size = 48,
  className = “”,
  messages = [],
  defaultRating = 0,
  onSetRating,
}) {
  const [rating, setRating] = useState(defaultRating);
  const [tempRating, setTempRating] = useState(0);

  function handleRating(rating) {
    setRating(rating);
    onSetRating(rating);
  }

  const textStyle = {
    lineHeight: “1”,
    margin: “0”,
    color,
    fontSize: `${size / 1.5}px`,
  };

  return (
    <div style={containerStyle} className={className}>
      <div style={starContainerStyle}>
        {Array.from({ length: maxRating }, (_, i) => (
          <Star
            key={i}
            onRate={() => handleRating(i + 1)}
            full={tempRating ? tempRating >= i + 1 : rating >= i + 1}
            onHoverIn={() => setTempRating(i + 1)}
            onHoverOut={() => setTempRating(0)}
            color={color}
            size={size}
          />
        ))}
      </div>
      <p style={textStyle}>
        {messages.length === maxRating
          ? messages[tempRating ? tempRating 1 : rating 1]
          : tempRating || rating || “”}
      </p>
    </div>
  );
}

function Star({ onRate, full, onHoverIn, onHoverOut, color, size }) {
  const starStyle = {
    width: `${size}px`,
    height: `${size}px`,
    display: “block”,
    cursor: “pointer”,
  };

  return (
    <span
      role=“button”
      style={starStyle}
      onClick={onRate}
      onMouseEnter={onHoverIn}
      onMouseLeave={onHoverOut}
    >
      {full ? (
        <svg
          xmlns=“http://www.w3.org/2000/svg”
          viewBox=“0 0 20 20”
          fill={color}
          stroke={color}
        >
          <path d=“M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z” />
        </svg>
      ) : (
        <svg
          xmlns=“http://www.w3.org/2000/svg”
          fill=“none”
          viewBox=“0 0 24 24”
          stroke={color}
        >
          <path
            strokeLinecap=“round”
            strokeLinejoin=“round”
            strokeWidth=“{2}”
            d=“M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z”
          />
        </svg>
      )}
    </span>
  );
}
import { useEffect } from “react”;

export function useKey(key, action) {
  useEffect(
    function () {
      function callback(e) {
        if (e.code.toLowerCase() === key.toLowerCase()) {
          action();
        }
      }
      document.addEventListener(“keydown”, callback);

      return function () {
        document.removeEventListener(“keydown”, callback);
      };
    },
    [action, key]
  );
}
import { useEffect, useState } from “react”;

 

export function useLocalStorageState(initialState, key) {
  const [value, setValue] = useState(function () {
    const storedWatched = localStorage.getItem(key);
    return storedWatched ? JSON.parse(storedWatched) : initialState;
  });
  // when watched changes this method gets called, so it is called by both
  // handleAddWatched and by handleDeleteWatched
  useEffect(
    function () {
      // the watched state has already been updated before this effect is called
      // so we can use the updated watched list and don’t have to add the movie
      // like we did in handleAddWatched
      localStorage.setItem(key, JSON.stringify(value));
    },
    [value, key]
  );

 

  return [value, setValue];
}
import { useEffect, useState } from “react”;

const KEY = “1caea674”;

export function useMovies(query, callback) {
  const [movies, setMovies] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(“”);

  useEffect(
    function () {
      //callback?.();

      const controller = new AbortController();

      async function fetchMovies() {
        try {
          setIsLoading(true);
          setError(“”);
          const res = await fetch(
            `http://www.omdbapi.com/?apikey=${KEY}&s=${query}`,
            { signal: controller.signal }
          );
          if (!res)
            throw new Error(“Something went wrong with fetching movies”);

          const data = await res.json();

          if (data.Response === “False”) throw new Error(“Movie not found”);

          setMovies(data.Search);
          setError(“”);
        } catch (err) {
          if (err.name !== “AbortError”) setError(err.message);
        } finally {
          setIsLoading(false);
        }
      }
      if (query.length < 3) {
        setMovies([]);
        setError(“”);
        return;
      }

      fetchMovies();

      // everytime a new request gets made this function aborts the above fetch call
      return function () {
        controller.abort();
      };
    },
    [query]
  );

  return { movies, isLoading, error };
}
:root {
  –color-primary: #6741d9;
  –color-primary-light: #7950f2;
  –color-text: #dee2e6;
  –color-text-dark: #adb5bd;
  –color-background-100: #343a40;
  –color-background-500: #2b3035;
  –color-background-900: #212529;
  –color-red: #fa5252;
  –color-red-dark: #e03131;
}

 

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

 

html {
  font-size: 62.5%;
}

 

body {
  font-family: -apple-system, BlinkMacSystemFont, “Segoe UI”, Roboto, Oxygen,
    Ubuntu, Cantarell, “Open Sans”, “Helvetica Neue”, sans-serif;

 

  color: var(–color-text);
  background-color: var(–color-background-900);
  padding: 2.4rem;
}

 

/* ******* */

 

.nav-bar {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  align-items: center;
  height: 7.2rem;
  padding: 0 3.2rem;
  background-color: var(–color-primary);
  border-radius: 0.9rem;
}

 

.main {
  margin-top: 2.4rem;
  height: calc(100vh 7.2rem 3 * 2.4rem);
  display: flex;
  gap: 2.4rem;
  justify-content: center;
}

 

.box {
  width: 42rem;
  max-width: 42rem;
  background-color: var(–color-background-500);
  border-radius: 0.9rem;
  overflow: scroll;
  position: relative;
}

 

.loader {
  text-align: center;
  text-transform: uppercase;
  font-size: 2rem;
  font-weight: 600;
  margin: 4.8rem;
}

 

.error {
  text-align: center;
  font-size: 2rem;
  padding: 4.8rem;
}

 

/* ******* */

 

.logo {
  display: flex;
  align-items: center;
  gap: 0.8rem;
}

 

.logo span {
  font-size: 3.2rem;
}

 

.logo h1 {
  font-size: 2.4rem;
  font-weight: 600;
  color: #fff;
}

 

.search {
  justify-self: center;
  border: none;
  padding: 1.1rem 1.6rem;
  font-size: 1.8rem;
  border-radius: 0.7rem;
  width: 40rem;
  transition: all 0.3s;
  color: var(–color-text);

 

  /* background-color: var(–color-background-900); */
  background-color: var(–color-primary-light);
}

 

.search::placeholder {
  color: var(–color-text-dark);
}

 

.search:focus {
  outline: none;
  box-shadow: 0 2.4rem 2.4rem rgba(0, 0, 0, 0.1);
  transform: translateY(-2px);
}

 

.num-results {
  justify-self: end;
  font-size: 1.8rem;
}

 

.btn-toggle {
  position: absolute;
  top: 0.8rem;
  right: 0.8rem;
  height: 2.4rem;
  aspect-ratio: 1;
  border-radius: 50%;
  border: none;
  background-color: var(–color-background-900);
  color: var(–color-text);
  font-size: 1.4rem;
  font-weight: bold;
  cursor: pointer;
  z-index: 999;
}

 

.list {
  list-style: none;
  padding: 0.8rem 0;
  overflow: scroll;
}

 

.list-watched {
  height: calc(100% 9rem);
}

 

.list li {
  position: relative;
  display: grid;
  grid-template-columns: 4rem 1fr;
  grid-template-rows: auto auto;
  column-gap: 2.4rem;
  font-size: 1.6rem;
  align-items: center;

 

  padding: 1.6rem 3.2rem;
  border-bottom: 1px solid var(–color-background-100);
}

 

.list.list-movies li {
  cursor: pointer;
  transition: all 0.3s;
}

 

.list.list-movies li:hover {
  background-color: var(–color-background-100);
}

 

.list img {
  width: 100%;
  grid-row: 1 / -1;
}

 

.list h3 {
  font-size: 1.8rem;
}

 

.list div {
  display: flex;
  align-items: center;
  gap: 2.4rem;
}

 

.list p {
  display: flex;
  align-items: center;
  gap: 0.8rem;
}

 

.btn-delete {
  position: absolute;
  right: 2.4rem;

 

  height: 1.8rem;
  aspect-ratio: 1;
  border-radius: 50%;
  border: none;
  background-color: var(–color-red);
  color: var(–color-background-900);
  font-size: 0.9rem;
  font-weight: bold;
  cursor: pointer;
  transition: all 0.3s;
}

 

.btn-delete:hover {
  background-color: var(–color-red-dark);
}

 

/* ******* */

 

.summary {
  padding: 2.2rem 3.2rem 1.8rem 3.2rem;
  border-radius: 0.9rem;
  background-color: var(–color-background-100);
  box-shadow: 0 1.2rem 2.4rem rgba(0, 0, 0, 0.2);
}

 

.summary h2 {
  text-transform: uppercase;
  font-size: 1.6rem;
  margin-bottom: 0.6rem;
}

 

.summary div {
  display: flex;
  align-items: center;
  gap: 2.4rem;
  font-size: 1.6rem;
  font-weight: 600;
}

 

.summary p {
  display: flex;
  align-items: center;
  gap: 0.8rem;
}

 

/* ******* */

 

.details {
  line-height: 1.4;
  font-size: 1.4rem;
}

 

.details header {
  display: flex;
}

 

.details section {
  padding: 4rem;
  display: flex;
  flex-direction: column;
  gap: 1.6rem;
}

 

.details img {
  width: 33%;
}

 

.details-overview {
  width: 100%;
  padding: 2.4rem 3rem;
  background-color: var(–color-background-100);
  display: flex;
  flex-direction: column;
  gap: 1.4rem;
}

 

.details-overview h2 {
  font-size: 2.4rem;
  margin-bottom: 0.4rem;
  line-height: 1.1;
}

 

.details-overview p {
  display: flex;
  align-items: center;
  gap: 0.8rem;
}

 

.rating {
  background-color: var(–color-background-100);
  border-radius: 0.9rem;
  padding: 2rem 2.4rem;
  margin-bottom: 0.8rem;
  font-weight: 600;
  display: flex;
  flex-direction: column;
  gap: 2.4rem;
}

 

.btn-add {
  background-color: var(–color-primary);
  color: var(–color-text);
  border: none;
  border-radius: 10rem;
  font-size: 1.4rem;
  padding: 1rem;
  font-weight: bold;
  cursor: pointer;
  transition: all 0.3s;
}

 

.btn-add:hover {
  background-color: var(–color-primary-light);
}

 

.seconds {
  background-color: var(–color-background-100);
  width: 8rem;
  border-radius: 10rem;
  font-weight: 600;
  text-align: center;
  padding: 0.4rem;
  margin-top: 2.4rem;
}

 

.btn-back {
  position: absolute;
  top: 0.6rem;
  left: 0.6rem;
  height: 3.2rem;
  aspect-ratio: 1;
  border-radius: 50%;
  border: none;
  /* background-color: var(–color-text); */
  background-color: #fff;
  color: var(–color-background-500);
  box-shadow: 0 8px 20px rgba(0, 0, 0, 0.8);
  font-family: sans-serif;
  font-size: 2.4rem;
  font-weight: bold;
  cursor: pointer;
  z-index: 999;
  display: flex;
  align-items: center;
  justify-content: center;
}

 

/*
SPACING SYSTEM (px)
2 / 4 / 8 / 12 / 16 / 24 / 32 / 40 / 48 / 64 / 80 / 96 / 128

 

FONT SIZE SYSTEM (px)
10 / 12 / 14 / 16 / 18 / 20 / 24 / 30 / 36 / 44 /52 / 62 / 74 / 86 / 98
*/

 

/*
FULL STAR

 

<svg
  xmlns=”http://www.w3.org/2000/svg”
  viewBox=”0 0 20 20″
  fill=”#000″
  stroke=”#000″
>
  <path
    d=”M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z”
  />
</svg>
EMPTY STAR

 

<svg
  xmlns=”http://www.w3.org/2000/svg”
  fill=”none”
  viewBox=”0 0 24 24″
  stroke=”#000″
>
  <path
    strokeLinecap=”round”
    strokeLinejoin=”round”
    strokeWidth=”{2}”
    d=”M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z”
  />
</svg>

 

*/