No tienes acceso a esta clase

¡Continúa aprendiendo! Únete y comienza a potenciar tu carrera

No se trata de lo que quieres comprar, sino de quién quieres ser. Invierte en tu educación con el precio especial

Antes: $249

Currency
$209

Paga en 4 cuotas sin intereses

Paga en 4 cuotas sin intereses
Suscríbete

Termina en:

11 Días
21 Hrs
10 Min
7 Seg

Reto: PlatziMovies con React Router

28/30
Recursos

Aportes 5

Preguntas 0

Ordenar por:

¿Quieres ver más aportes, preguntas y respuestas de la comunidad?

Hola 😁! Les comparto mi resultado final, cualquier recomendación o aporte es bienvenido.

  1. Despliegue: https://pchaparro-platzimovies-react.netlify.app/

  2. Código: https://github.com/PChaparro/platzimovies-react

Ya lo tenía planeado 👀 en un futuro voy a trabajar a full mi platzi movies con muchas más herramientas. TMDB tiene una gran API, muy recomendada para proyectos profesionales.

Hola a todos 😃 Aquí mi proyecto de platzi movies con react 😃
-Repo:
https://github.com/DavidEspinoG/platzi_movies_react
-Deploy:
https://davidespinog.github.io/platzi_movies_react/#/

Solución

.
GitHub: https://github.com/HaroldZS/themoviedb-api-rest-app
Deploy: https://haroldzs.github.io/themoviedb-api-rest-app/
.

.
Para resolver el reto hemos utilizado las siguientes versiones de React y React Router DOM:
.

"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.24.0",

.
Nuestra estructura de carpetas es la siguiente:
.

.
El archivo src/index.js.
.

import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

.
El archivo src/index.css contienes solo los estilos compartidos entre toda la aplicación. Se pueden ver más a detalle en el repositorio de GitHub correspondiente.
.

/* General */
* {
  box-sizing: border-box;
}

:root {
  --purple-light-1: #fbfafb;
  --purple-light-2: #eeeaf2;
  --purple-medium-1: #aa83c8;
  --purple-medium-2: #8b48bf;
  --purple-medium-3: #5c218a;
  --purple-dark-1: #3e0f64;
  --purple-dark-2: #2a0646;

  --yellow: #eecc75;
  --green: #cad297;
  --aqua: #b7eac5;
  --lightBlue: #a2eee5;
  --darkBlue: #8ea2ea;
  --red: #f09d9d;

  --font-family-titles: "Dosis", sans-serif;
  --font-family-text: "Red Hat Display", sans-serif;
  --font-weight-title1: 800;
  --font-weight-title2: 700;
  --font-weight-text1: 400;
  --font-weight-text2: 500;
}

html {
  background-color: var(--purple-medium-3);
}

// Otros estilos

/* Shared */
.header-container,
.trendingPreview-header,
.categoriesPreview-container,
.liked-header {
  padding: 0 24px;
}

// Más estilos

/* Navigation */
.inactive {
  display: none !important;
}

@keyframes loading-skeleton {
  0%,
  100% {
    opacity: 100%;
  }
  50% {
    opacity: 0%;
  }
}

.
En el archivo App.js es donde implementamos nuestro HashRouter.
.

import { HashRouter, Route, Routes } from "react-router-dom";
import { HomePage } from "./routes/HomePage";
import { SearchMoviePage } from "./routes/SearchMoviePage";
import { MovieDetailPage } from "./routes/MovieDetailPage";
import { TrendsPage } from "./routes/TrendsPage";
import { CategoryPage } from "./routes/CategoryPage";
import { NotFoundPage } from "./routes/NotFoundPage";

function App() {
  return (
    <HashRouter>
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="/search" element={<SearchMoviePage />} />
        <Route path="/movies/:movieId" element={<MovieDetailPage />} />
        <Route path="/trends" element={<TrendsPage />} />
        <Route path="/categories/:categoryId" element={<CategoryPage />} />
        <Route path="*" element={<NotFoundPage />} />
      </Routes>
    </HashRouter>
  );
}

export default App;

.
Dentro de Routes tenemos a nuestras rutas.
.

.
Cada uno de estas rutas contiene un archivo index.js que es el componente o página que vamos a renderizar dependiendo de la ruta a la que vamos a acceder.
.
Dentro de hook tenemos a nuestros custom hooks.
.

.
Dentro de componentes se encuentran los componentes generales que conforman las páginas o rutas en routes.
.

.
Cada uno de estos componentes contiene un archivo index.js y su correspondiente hoja de estilos.
.
A continuación mostraremos algunos de los archivos más importantes de la aplicación.
.
Omitimos la explicación de las hojas de estilos porque simplemente se movieron los estilos hacia los componentes pertinentes. Sin embargo, estas se pueden ver más a detalle en el repositorio de GitHub correspondiente.
.

Hooks

.
El react hook useTMDBApi.
.

import { useState, useEffect } from "react";

const useTMDBApi = (endpoint, params = {}) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [maxPage, setMaxPage] = useState(1);

  const buildUrl = () => {
    let urlParams = "";
    if (params && Object.keys(params).length > 0) {
      urlParams = Object.keys(params)
        .map((key) => `${key}=${params[key]}`)
        .join("&");
    }
    return `https://api.themoviedb.org/3/${endpoint}?api_key=${process.env.REACT_APP_API_KEY}&${urlParams}`;
  };

  const url = buildUrl();

  useEffect(() => {
    const fetchData = async () => {
      try {
        setLoading(true);
        const response = await fetch(url);
        if (!response.ok) {
          throw new Error("Network response was not ok");
        }
        const result = await response.json();
        setMaxPage(result.total_pages);
        setData(result);
      } catch (error) {
        setError(error);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [url]);

  return { data, loading, error, maxPage };
};

export { useTMDBApi };

.
El hook useTMDBApi facilita la obtención de datos de la API de The Movie Database (TMDB) desde un componente React. Acepta un endpoint y parámetros opcionales params, construye la url de la solicitud y administra el estado de la carga, los datos, el error y la página máxima maxPage para poder hacer un control sobre la paginación. Utiliza useEffect para realizar la solicitud cuando cambia la URL, actualizando los estados correspondientes según el resultado de la solicitud. Devuelve un objeto con los datos obtenidos, el estado de carga, el error y el número máximo de páginas disponibles en la respuesta.
.
El react hook useObserver.
.

import { useEffect, useRef } from "react";

const useObserver = (callback, className) => {
  const elementRef = useRef(null);

  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting) {
            callback();
          }
        });
      },
      { threshold: 0.1 }
    );

    const currentElement = elementRef.current;
    if (currentElement) {
      observer.observe(currentElement);
    }

    return () => {
      if (currentElement) {
        observer.unobserve(currentElement);
      }
    };
  }, [callback, className]);

  return elementRef;
};

export { useObserver };

.
El hook useObserver utiliza la API IntersectionObserver para observar si un elemento del DOM es visible en la ventana del navegador (viewport). Cuando el elemento se vuelve visible, ejecuta una función de callback. El hook devuelve una referencia elementRef que debe asignarse al elemento del DOM que se quiere observar. Además, limpia el observador cuando el componente se desmonte o cuando cambian las dependencias.
.
El react hook useLocalStorage.
.

import { useEffect, useState } from "react";

function useLocalStorage(tag, initialState) {
  const storedValue = JSON.parse(localStorage.getItem(tag));
  const [item, setItem] = useState(
    storedValue !== null ? storedValue : initialState
  );

  const getStorageItem = () => {
    return JSON.parse(localStorage.getItem(tag));
  };

  const setStorageItem = (newItem) => {
    localStorage.setItem(tag, JSON.stringify(newItem));
    setItem(newItem);
  };

  const addItem = (newItem) => {
    const items = getStorageItem() || initialState;
    if (Array.isArray(items)) {
      const newArray = [...items, newItem];
      setStorageItem(newArray);
    } else {
      setStorageItem(newItem);
    }
  };

  useEffect(() => {
    if (storedValue === null) {
      setStorageItem(initialState);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [tag, initialState, storedValue]);

  return {
    item,
    getStorageItem,
    setStorageItem,
    addItem,
  };
}

export { useLocalStorage };

.
El hook useLocalStorage gestiona el estado sincronizado con el almacenamiento local del navegador localStorage. Inicializa el estado con un valor almacenado en localStorage o un valor inicial proporcionado initialState. Proporciona funciones para obtener, establecer y agregar elementos al almacenamiento local, asegurando que las actualizaciones en localStorage se reflejen en el estado del componente. Además, establece el valor inicial en localStorage si no está presente. El hook devuelve el estado actual item y las funciones para interactuar con el almacenamiento local.
.
El hook useLikedMovies.
.

import { useLocalStorage } from "../hook/useLocalStorage";

const useLikedMovies = () => {
  const {
    item: likedMovies,
    addItem: addLikedMovie,
    setStorageItem: setLikedMovie,
  } = useLocalStorage("liked_movies", []);

  const likeMovie = (e, movie) => {
    e.stopPropagation();
    const likedMovieIndex = likedMovies.findIndex(
      (likedMovie) => likedMovie.id === movie.id
    );

    if (likedMovieIndex !== -1) {
      const newLikedMovies = likedMovies.filter(
        (likedMovie) => likedMovie.id !== movie.id
      );
      setLikedMovie(newLikedMovies);
    } else {
      addLikedMovie(movie);
    }
  };

  return {
    likedMovies,
    likeMovie,
  };
};

export { useLikedMovies };

.
El hook useLikedMovies gestiona una lista de películas favoritas almacenadas en localStorage. Utiliza el hook useLocalStorage para inicializar, agregar y establecer películas en la lista de favoritos bajo la clave “liked_movies”. Proporciona una función likeMovie que añade una película a la lista de favoritos si no está ya en ella, o la elimina si ya está presente. Este hook devuelve la lista de películas favoritas likedMovies y la función likeMovie para actualizar esta lista.
.
El hook useLazyLoading.
.

import { useEffect, useRef } from "react";

const useLazyLoading = (items) => {
  const imgRefs = useRef([]);

  useEffect(() => {
    const lazyLoader = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          const img = entry.target;
          const url = img.getAttribute("data-img");
          img.setAttribute(
            "src",
            url.endsWith("null")
              ? "https://static.platzi.com/static/images/error/img404.png"
              : url
          );
          lazyLoader.unobserve(img);
        }
      });
    });

    imgRefs.current.forEach((img) => {
      if (img) {
        lazyLoader.observe(img);
      }
    });

    return () => {
      // eslint-disable-next-line
      imgRefs.current.forEach((img) => {
        if (img) {
          lazyLoader.unobserve(img);
        }
      });
    };
  }, [items]);

  return imgRefs;
};

export { useLazyLoading };

.
El hook useLazyLoading permite la carga diferida de imágenes en una lista de elementos items. Utiliza una referencia mutable imgRefs para almacenar referencias a las imágenes y la API IntersectionObserver para observar cuándo las imágenes se vuelven visibles en la ventana del navegador (viewport). Cuando una imagen se vuelve visible, se carga su URL desde un atributo “data-img”, y si la URL termina en “null”, se reemplaza con una imagen de error. Una vez cargada la imagen, se deja de observar. El hook devuelve la referencia de imágenes imgRefs que debe asignarse a cada imagen en el componente que utiliza este hook.
.

Routes

.
La página de HomePage.
.


.

import React from "react";
import { Header } from "../../components/Header";
import { TrendingPreview } from "../../components/TrendingPreview";
import { CategoriesPreview } from "../../components/CategoriesPreview";
import { LikedMovies } from "../../components/LikedMovies";
import { Footer } from "../../components/Footer";
import { useTMDBApi } from "../../hook/useTMDBApi";
import { useLikedMovies } from "../../hook/useLikedMovies";

function HomePage() {
  const { likedMovies, likeMovie } = useLikedMovies();
  const { data: categoryPreviewData, loading: loadingCategories } =
    useTMDBApi("genre/movie/list");
  const { data: trendingPreviewData, loading: loadingTrending } =
    useTMDBApi("trending/movie/day");

  return (
    <>
      <Header />
      <TrendingPreview
        movies={loadingTrending ? null : trendingPreviewData.results}
        likeMovie={likeMovie}
        likedMovies={likedMovies}
      />
      <CategoriesPreview
        categories={loadingCategories ? null : categoryPreviewData.genres}
      />
      <LikedMovies
        likeMovie={likeMovie}
        likedMovies={likedMovies.length > 0 ? likedMovies : null}
      />
      <Footer />
    </>
  );
}

export { HomePage };

.
El componente HomePage combina varios componentes para crear la página de inicio de la aplicación. Utiliza el hook useTMDBApi para obtener datos de categorías y tendencias de películas desde la API de TMDB, y el hook useLikedMovies para gestionar las películas favoritas. Renderiza los componentes Header, TrendingPreview, CategoriesPreview, LikedMovies y Footer, pasando los datos obtenidos y las funciones necesarias como props. Muestra un listado de películas en tendencia, categorías de películas y las películas favoritas del usuario.
.
La página de CategoryPage.
.

.

import React, { useState, useEffect } from "react";
import { useLikedMovies } from "../../hook/useLikedMovies";
import { useTMDBApi } from "../../hook/useTMDBApi";
import { Header } from "../../components/Header";
import { GenericList } from "../../components/GenericList";
import { useParams } from "react-router-dom";

function CategoryPage() {
  const params = useParams();
  const [page, setPage] = useState(1);
  const [results, setResults] = useState([]);
  const { likedMovies, likeMovie } = useLikedMovies();

  const {
    data: moviesByCategoryData,
    loading: loadingMoviesByCategory,
    maxPage,
  } = useTMDBApi("discover/movie", { with_genres: params.categoryId, page });

  useEffect(() => {
    if (moviesByCategoryData && moviesByCategoryData.results) {
      setResults((prevResults) => [
        ...prevResults,
        moviesByCategoryData.results,
      ]);
    }
  }, [moviesByCategoryData]);

  const addNextPage = () => {
    setPage((prevPage) => prevPage + 1);
  };

  return (
    <>
      <Header />
      <GenericList
        movies={results}
        loading={loadingMoviesByCategory}
        likeMovie={likeMovie}
        likedMovies={likedMovies}
        addNextPage={addNextPage}
        page={page}
        maxPage={maxPage}
      />
    </>
  );
}

export { CategoryPage };

.
El componente CategoryPage muestra películas basadas en una categoría específica seleccionada por el usuario. Utiliza hooks de React y hooks personalizados como useTMDBApi para obtener los datos de las películas por categoría y useLikedMovies para gestionar las películas favoritas. Los datos se actualizan y almacenan en el estado local utilizando useState y useEffect. Utiliza el componente GenericList para mostrar las películas y permite cargar más películas al desplazarse hacia abajo mediante una función de paginación.
.
La página de MovieDetailPage.
.


.

import React from "react";
import { Header } from "../../components/Header";
import { MovieDetail } from "../../components/MovieDetail";
import { useParams } from "react-router-dom";
import { useTMDBApi } from "../../hook/useTMDBApi";
import { useLikedMovies } from "../../hook/useLikedMovies";

function MovieDetailPage() {
  const params = useParams();
  const { likedMovies, likeMovie } = useLikedMovies();
  const { data: movieData, loading: loadingMovie } = useTMDBApi(
    `movie/${params.movieId}`
  );
  const { data: relatedMovieData, loading: loadingRelatedMovie } = useTMDBApi(
    `movie/${params.movieId}/recommendations`
  );

  return (
    <>
      <Header moviePoster={loadingMovie ? null : movieData.poster_path} />
      <MovieDetail
        movieData={loadingMovie ? null : movieData}
        relatedMovies={loadingRelatedMovie ? null : relatedMovieData.results}
        likeMovie={likeMovie}
        likedMovies={likedMovies}
        categories={loadingMovie ? null : movieData.genres}
      />
    </>
  );
}

export { MovieDetailPage };

.
El componente MovieDetailPage muestra los detalles de una película seleccionada, así como las recomendaciones de películas relacionadas. Utiliza useParams para obtener el ID de la película de la URL, y hooks personalizados como useTMDBApi para obtener los datos de la película y las recomendaciones desde la API de TMDB. También utiliza useLikedMovies para gestionar las películas favoritas. Renderiza el componente Header con la imagen del póster de la película y el componente MovieDetail para mostrar los detalles y las películas relacionadas.
.
La página de NotFoundPage.
.

import React from "react";
import "./NotFoundPage.css";

function NotFoundPage() {
  return <div>NotFoundPage</div>;
}

export { NotFoundPage };

.
El componente NotFoundPage es un componente simple que muestra un mensaje indicando que la página no se encontró.
.
La página de SearchMoviePage.
.

.

import React, { useState, useEffect } from "react";
import { Header } from "../../components/Header";
import { useLocation, useSearchParams } from "react-router-dom";
import { useTMDBApi } from "../../hook/useTMDBApi";
import { GenericList } from "../../components/GenericList";
import { useLikedMovies } from "../../hook/useLikedMovies";

function SearchMoviePage() {
  const location = useLocation();
  const [searchParams] = useSearchParams();
  const query = location.state || searchParams.get("query");
  const [page, setPage] = useState(1);
  const [results, setResults] = useState([]);
  const { likedMovies, likeMovie } = useLikedMovies();

  const {
    data: searchData,
    loading: loadingSearchData,
    maxPage,
  } = useTMDBApi("search/movie", { query, page });

  useEffect(() => {
    setResults([]);
    setPage(1);
  }, [query]);

  useEffect(() => {
    if (searchData && searchData.results) {
      if (page === 1) {
        setResults([]);
        setPage(1);
      }
      setResults((prevResults) => [...prevResults, searchData.results]);
    }
    // eslint-disable-next-line
  }, [searchData]);

  const addNextPage = () => {
    setPage(page + 1);
  };

  return (
    <>
      <Header />
      <GenericList
        movies={results}
        loading={loadingSearchData}
        likeMovie={likeMovie}
        likedMovies={likedMovies}
        addNextPage={addNextPage}
        page={page}
        maxPage={maxPage}
      />
    </>
  );
}

export { SearchMoviePage };

.
El componente SearchMoviePage permite a los usuarios buscar películas utilizando una barra de búsqueda. Utiliza useLocation y useSearchParams para obtener la consulta de búsqueda desde la URL o el estado de la navegación. Almacena los resultados de la búsqueda y la página actual en el estado local utilizando useState. Utiliza useTMDBApi para obtener datos de la API de TMDB y useLikedMovies para gestionar las películas favoritas. Cuando cambia la consulta de búsqueda, se restablecen los resultados y la página. Renderiza el componente Header y GenericList para mostrar los resultados de la búsqueda y permite la paginación para cargar más resultados.
.
La página de TrendsPage.
.

.

import React, { useEffect, useState } from "react";
import { Header } from "../../components/Header";
import { GenericList } from "../../components/GenericList";
import { useTMDBApi } from "../../hook/useTMDBApi";
import { useLikedMovies } from "../../hook/useLikedMovies";

function TrendsPage() {
  const [page, setPage] = useState(1);
  const [results, setResults] = useState([]);
  const { likedMovies, likeMovie } = useLikedMovies();

  const {
    data: trendingData,
    loading: loadingTrending,
    maxPage,
  } = useTMDBApi("trending/movie/day", { page });

  useEffect(() => {
    if (trendingData && trendingData.results) {
      setResults((prevResults) => [...prevResults, trendingData.results]);
    }
  }, [trendingData]);

  const addNextPage = () => {
    setPage((prevPage) => prevPage + 1);
  };

  return (
    <>
      <Header />
      <GenericList
        movies={results}
        loading={loadingTrending}
        likeMovie={likeMovie}
        likedMovies={likedMovies}
        addNextPage={addNextPage}
        page={page}
        maxPage={maxPage}
      />
    </>
  );
}

export { TrendsPage };

.
El componente TrendsPage presenta las películas en tendencia del día utilizando la API de TMDB. Utiliza el hook useTMDBApi para obtener datos de las películas en tendencia, manejando el estado de carga y la paginación. También emplea useLikedMovies para gestionar las películas favoritas. Se mantiene el estado local de la página actual y los resultados obtenidos, actualizándose cuando se reciben nuevos datos de la API. El método addNextPage incrementa la página actual para cargar más resultados. El componente renderiza un Header para la navegación y un GenericList para mostrar las películas en tendencia, permitiendo la interacción con la lista de favoritos y la carga de más contenido conforme el usuario navega.
.

Components

.
El componente Movie.
.

import React from "react";
import "./Movie.css";

function Movie({ movie, index, navigate, imgRefs, likedMoviesIds, likeMovie }) {
  return (
    <div
      className="movie-container"
      key={movie.id}
      onClick={() => navigate(`/movies/${movie.id}`)}
    >
      <img
        data-img={`https://image.tmdb.org/t/p/w300/${movie.poster_path}`}
        className="movie-img"
        alt={movie.title}
        ref={(el) => (imgRefs.current[index] = el)}
      />
      <button
        className={`movie-btn ${
          likedMoviesIds.includes(movie.id) && "movie-btn--liked"
        }`}
        onClick={(e) => likeMovie(e, movie)}
      ></button>
    </div>
  );
}

export { Movie };

.
El componente Movie representa una tarjeta de película individual con su imagen y un botón para marcarla como favorita. Cuando se hace clic en la tarjeta, se navega a una página de detalles de la película utilizando navigate. La imagen de la película se carga perezosamente utilizando una referencia imgRefs. El botón de favorito cambia su estilo si la película está en la lista de películas favoritas likedMoviesIds y permite agregar o eliminar la película de esa lista con la función likeMovie.
.
El componente MovieList.
.

import React from "react";
import { useLazyLoading } from "../../hook/useLazyLoading";
import { useNavigate } from "react-router-dom";
import { Movie } from "../Movie";
import "./MovieList.css";

function MovieList({ movies, likeMovie, likedMovies }) {
  const navigate = useNavigate();
  const likedMoviesIds = likedMovies.length > 0
    ? likedMovies.map((movie) => movie.id)
    : [];
  const imgRefs = useLazyLoading(movies);

  return (
    <>
      {movies ? (
        <>
          {movies.map((movie, index) => (
            <Movie
              key={movie.id}
              movie={movie}
              index={index}
              navigate={navigate}
              imgRefs={imgRefs}
              likedMoviesIds={likedMoviesIds}
              likeMovie={likeMovie}
            />
          ))}
        </>
      ) : (
        Array(3)
          .fill()
          .map((_, index) => (
            <div
              key={index}
              className="movie-container movie-container--loading"
            ></div>
          ))
      )}
    </>
  );
}

export { MovieList };

.
El componente MovieList muestra una lista de películas. Utiliza el hook useLazyLoading para cargar las imágenes de las películas de manera diferida. Navega a los detalles de la película cuando se hace clic en una tarjeta de película y permite marcar o desmarcar películas como favoritas. Si no hay películas para mostrar, muestra un loading skeleton de la lista de películas.
.
El componente Category.
.

import React from "react";
import { useNavigate } from "react-router-dom";
import "./Category.css"

function Category({ categories }) {
  const navigate = useNavigate();

  return (
    <>
      {categories ? (
        <>
          {categories.map((category) => (
            <div className="category-container" key={category.id}>
              <h3
                className="category-title"
                id={`id${category.id}`}
                onClick={() =>
                  navigate(
                    `/categories/${category.id}?category=${category.name}`
                  )
                }
              >
                {category.name}
              </h3>
            </div>
          ))}
        </>
      ) : (
        <>
          <div className="category-container category-container--loading"></div>
          <div className="category-container category-container--loading"></div>
          <div className="category-container category-container--loading"></div>
        </>
      )}
    </>
  );
}

export { Category };

.
El componente Category muestra una lista de categorías de películas. Cada categoría tiene un título que, al hacer clic, navega a una página de lista de películas filtrada por esa categoría usando useNavigate. Si no hay categorías disponibles, muestra un loading skeleton.
.
El componente LikedMovies.
.

import React from "react";
import { MovieList } from "../MovieList";

function LikedMovies({ likeMovie, likedMovies }) {
  return (
    <section id="liked" className="liked-container">
      <div className="liked-header">
        <h2 className="liked-title">Favorite movies</h2>
      </div>

      <article className="liked-movieList">
        {likedMovies ? (
          <MovieList
            movies={likedMovies}
            likeMovie={likeMovie}
            likedMovies={likedMovies}
          />
        ) : (
          Array(3)
            .fill()
            .map((_, index) => (
              <div class="movie-container">
                <img
                  key={index}
                  src="https://image.tmdb.org/t/p/w300/adOzdWS35KAo21r9R4BuFCkLer6.jpg"
                  class="movie-img"
                  alt="Nombre de la película"
                />
              </div>
            ))
        )}
      </article>
    </section>
  );
}

export { LikedMovies };

.
El componente LikedMovies muestra una lista de películas que han sido marcadas como favoritas. Utiliza el componente MovieList para renderizar las películas favoritas, pasando la función likeMovie y la lista de películas favoritas como props. Si no hay películas favoritas, muestra un estado de carga con imágenes de películas por defecto.
.
El componente Header.
.

import React, { useEffect, useState } from "react";
import "./Header.css";
import { useLocation, useNavigate, useSearchParams } from "react-router-dom";

function Header({ moviePoster = null }) {
  const location = useLocation();
  const navigate = useNavigate();

  const [searchValue, setSearchValue] = useState("");
  const [searchParams] = useSearchParams();
  const category = searchParams.get("category");
  const searchQuery = searchParams.get("query");

  const isMovieDetailPage = location.pathname.startsWith("/movies/");
  const isTrendsPage = location.pathname.startsWith("/trends");
  const isCategoryPage = location.pathname.startsWith("/categories/");
  const isSearchPage = location.pathname.startsWith("/search");
  const isHomePage = location.pathname === "/";

  const isCategoryOrTrendPage = isTrendsPage || isCategoryPage;
  const isSearchOrHomePage = isSearchPage || isHomePage;

  const onSearchValueChange = ({ target: { value } }) => {
    setSearchValue(value);
  };

  const onSubmit = (e) => {
    e.preventDefault();
    navigate(`/search?query=${searchValue}`, { state: searchValue });
  };

  useEffect(() => {
    if (searchQuery) {
      setSearchValue(searchQuery);
    }
    // eslint-disable-next-line
  }, [searchQuery]);

  const goBack = () => {
    navigate(-1);
  };

  return (
    <header
      id="header"
      className={`header-container ${
        isMovieDetailPage && "header-container--long"
      }`}
      style={
        moviePoster && {
          background: `linear-gradient(180deg, rgba(0, 0, 0, 0.35) 19.27%, rgba(0, 0, 0, 0) 29.17%), url(https://image.tmdb.org/t/p/w500${moviePoster})`,
        }
      }
    >
      <span
        className={`header-arrow ${isHomePage && "inactive"} ${
          isMovieDetailPage && "header-arrow--white"
        }`}
        onClick={goBack}
      >
        &lt;
      </span>
      <h1 className={`header-title ${!isHomePage && "inactive"}`}>Movies</h1>
      <h1
        className={`header-title header-title--categoryView ${
          !isCategoryOrTrendPage && "inactive"
        }`}
      >
        {category}
      </h1>

      <form
        id="searchForm"
        className={`header-searchForm ${!isSearchOrHomePage && "inactive"}`}
        onSubmit={onSubmit}
      >
        <input
          type="text"
          placeholder="Avengers"
          value={searchValue}
          onChange={onSearchValueChange}
        />
        <button id="searchBtn" type="submit">
          🔍
        </button>
      </form>
    </header>
  );
}

export { Header };

.
El componente Header es un encabezado dinámico que cambia su contenido y estilo en función de la ruta actual. Utiliza hooks de React Router para gestionar la navegación y los parámetros de búsqueda. Muestra un título, una flecha para volver atrás y un formulario de búsqueda, cuyo estado se actualiza según la consulta de búsqueda en la URL. Si moviePoster está presente, establece una imagen de fondo en el encabezado. Este componente se adapta a diferentes páginas como detalles de películas, tendencias, categorías, búsqueda y la página de inicio.
.
El componente TrendingPreview.
.

import React from "react";
import "./TrendingPreview.css";
import { useNavigate } from "react-router-dom";
import { MovieList } from "../MovieList";

function TrendingPreview({ movies, likeMovie, likedMovies }) {
  const navigate = useNavigate();

  return (
    <section id="trendingPreview" className="trendingPreview-container">
      <div className="trendingPreview-header">
        <h2 className="trendingPreview-title">Trends</h2>
        <button
          className="trendingPreview-btn"
          onClick={() => navigate("/trends")}
        >
          Watch more
        </button>
      </div>

      <article className="trendingPreview-movieList">
        <MovieList
          movies={movies}
          likeMovie={likeMovie}
          likedMovies={likedMovies}
        />
      </article>
    </section>
  );
}

export { TrendingPreview };

.
El componente TrendingPreview muestra una vista previa de las películas en tendencia. Incluye un encabezado con un título y un botón que navega a una página de tendencias completa al hacer clic. Utiliza el componente MovieList para renderizar una lista de películas, pasando las películas en tendencia, la función para marcar como favorita y la lista de películas favoritas como props.
.
El componente CategoriesPreview.
.

import React from "react";
import { Category } from "../Category";

function CategoriesPreview({ categories }) {
  return (
    <section id="categoriesPreview" className="categoriesPreview-container">
      <h2 className="categoriesPreview-title">Categories</h2>

      <article className="categoriesPreview-list">
        <Category categories={categories} />
      </article>
    </section>
  );
}

export { CategoriesPreview };

.
El componente CategoriesPreview muestra una vista previa de las categorías de películas. Incluye un título y utiliza el componente Category para renderizar una lista de categorías. Cada categoría se muestra en un contenedor y permite la navegación a una página de lista de películas filtrada por esa categoría.
.
El componente Footer.
.

import React from "react";
import "./Footer.css";

function Footer() {
  return <footer>Powered by HaroldZS</footer>;
}

export { Footer };

.
El componente Observer.
.

import React from "react";
import { useObserver } from "../../hook/useObserver";
import "./Observer.css";

function Observer({ callback }) {
  const observerRef = useObserver(callback, "observer");

  return <div ref={observerRef} className="observer"></div>;
}

export { Observer };

.
El componente Observer utiliza el hook useObserver para detectar cuándo el div con la clase “observer” entra en el viewport. Este componente toma una función callback que se ejecuta cuando el elemento es visible en la pantalla. Utiliza una referencia observerRef que se pasa al div, activando así la observación y ejecución de la callback cuando el div se intersecta con el viewport.
.
El componente GenericList.
.

import React from "react";
import "./GenericList.css";
import { MovieList } from "../MovieList";
import { Observer } from "../Observer";

function GenericList({
  movies,
  loading,
  likeMovie,
  likedMovies,
  addNextPage,
  page,
  maxPage,
}) {
  return (
    <section id="genericList" className="genericList-container">
      {movies.map((movieList, index) => (
        <MovieList
          key={index}
          movies={loading ? null : movieList}
          likeMovie={likeMovie}
          likedMovies={likedMovies}
        />
      ))}
      {!loading && page <= maxPage && movies.length > 0 && (
        <Observer callback={addNextPage} />
      )}
    </section>
  );
}

export { GenericList };

.
El componente GenericList muestra listas genéricas de películas usando el componente MovieList. Si está cargando, no muestra las películas; si no, renderiza las películas en páginas sucesivas. Además, utiliza el componente Observer para cargar más contenido cuando el usuario se desplaza al final de la lista. Si no está cargando y aún hay más páginas por cargar, el Observer activa la función addNextPage para obtener más películas.
.
El componente MovieDetail.
.

import React from "react";
import "./MovieDetail.css";
import { MovieList } from "../MovieList";
import { Category } from "../Category";

function MovieDetail({
  movieData,
  relatedMovies,
  likeMovie,
  likedMovies,
  categories,
}) {
  return (
    <>
      <section id="movieDetail" className="movieDetail-container">
        {movieData ? (
          <>
            <h1 className="movieDetail-title">{movieData.title}</h1>
            <span className="movieDetail-score">{movieData.vote_average}</span>
            <p className="movieDetail-description">{movieData.overview}</p>
          </>
        ) : (
          <>
            <h1 className="movieDetail-title">Deadpool</h1>
            <span className="movieDetail-score">7.6</span>
            <p className="movieDetail-description">
              Wisecracking mercenary Deadpool battles the evil and powerful
              Cable and other bad guys to save a boy's life.
            </p>
          </>
        )}

        <article className="categories-list">
          <Category categories={categories} />
        </article>

        <article className="relatedMovies-container">
          <h2 className="relatedMovies-title">Related movies</h2>
          <div className="relatedMovies-scrollContainer">
            <MovieList
              movies={relatedMovies}
              likeMovie={likeMovie}
              likedMovies={likedMovies}
            />
          </div>
        </article>
      </section>
    </>
  );
}

export { MovieDetail };

.
El componente MovieDetail muestra los detalles de una película seleccionada, incluyendo su título, puntuación y descripción. También muestra una lista de categorías relacionadas utilizando el componente Category y una lista de películas relacionadas con el componente MovieList. Si no hay datos de la película disponibles, muestra un ejemplo con información de “Deadpool”.
.
Esta es la solución que implementé, aunque hay muchos aspectos que todavía puedo mejorar.

Reto: PlatziMovies con React Router

El reto consiste en agarrar el proyecto de Platzi Movies el que construimos en los cursos de Consumo de API REST con Javascript en el práctico y en el profesional, para replicarlo en React JS.
.
Esta aplicación tiene navegación, infinite scrolling, lazy loading entre otras cosas, donde incluso teníamos nuestro propio router hecho a mano para simular un hash router que nos permita realizar navegación a partir de un hash.
.
Llamábamos a location.hash para saber cuando renderizar una ruta u otra. Entonces debes replicar ese mísmo comportamiento pero con React y React Router DOM.