Hola 😁! Les comparto mi resultado final, cualquier recomendación o aporte es bienvenido.
Fundamentos de navegación en la web
¿Cuándo necesitas React Router?
SSR vs. Single Page Applications
Versiones de React Router: ¿Por qué son tantas? ¿Cuál elegir?
Introducción a React Router DOM 6
Instalación de React Router DOM 6
BrowserRouter vs. HashRouter
Route: componentes de navegación
Link vs. NavLink
useParams: rutas dinámicas
useNavigate: historial de navegación
Outlet: nested routes
Fake authentication con React Router DOM 6
useAuth: login y logout
Menú con rutas públicas y privadas
Navigate y redirects: protegiendo rutas privadas
Roles y permisos
Reto: composición de componentes con navegación
Reto: UX de login y logout
Reto: roles complejos
React Router en TODO Machine
Integrando React Router a proyectos en React
Creando las rutas de TODO Machine
Botón de editar TODOs
Generador automático de IDs
Cambiando modales por navegación
Obtener y editar TODOs
useLocation: transferencia de datos por navegación
Deploy con React Router en GitHub Pages
Próximos pasos
Reto: página de búsquedas con navegación
Reto: TODO Machine con React Router DOM 5
Reto: PlatziMovies con React Router
Reto: crea tu propio React Router
Espera más cursos de React.js
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
Paga en 4 cuotas sin intereses
Termina en:
Juan David Castro Gallego
Aportes 5
Preguntas 0
Hola 😁! Les comparto mi resultado final, cualquier recomendación o aporte es bienvenido.
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/#/
.
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.
.
.
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.
.
.
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.
.
.
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}
>
<
</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.
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
.
¿Quieres ver más aportes, preguntas y respuestas de la comunidad?