Hola 😁! Les comparto mi resultado final, cualquier recomendación o aporte es bienvenido.
Fundamentos de navegación en la web
Navegación Web con React Router: Fundamentos y Prácticas Avanzadas
Server-side Rendering vs Single Page Applications: Ventajas y Desventajas
Uso de React Router DOM 6 en Proyectos React
Introducción a React Router DOM 6
Instalación de React Router DOM 6 en un proyecto React
Uso de Hash Router en Aplicaciones Web con React Router DOM
Creación de Rutas Dinámicas con React Router DOM 6
Navegación en React Router: Uso de Link y NavLink
Rutas dinámicas con React Router DOM y useParams
Uso de useNavigate en React Router DOM para navegación dinámica
Uso de Outlet y Nested Routes en React Router DOM 6
Fake authentication con React Router DOM 6
Autenticación y Autorización en Apps con React Router y Hooks
Control de Acceso en Menú con Autenticación React
Protección de Rutas con React Router y Hooks
Roles y permisos en aplicaciones web: Autenticación y autorización
Retos avanzados en React: manejo de estado y composición de componentes
Mejorando la Redirección Post-Login en Aplicaciones Web
Roles y Permisos Avanzados en React Router v6
React Router en TODO Machine
Migración de Todo Machine a React Router 6
Organización de carpetas y rutas en React con React Router DOM 6
Maquetación de Botón Editar en Lista de Tareas con React
Generación de IDs únicos para gestionar tareas en React
Migración de modales a rutas en React: implementación práctica
Editar ToDos en React con Custom Hook y URL Parameters
Mejora de la Experiencia del Usuario al Editar To Do's en React
Implementación de React Router en Proyectos Legacy
Próximos pasos
Filtrado de Búsquedas en URL con React Router
Migración de React Router: de la versión 6 a la 5 en proyectos empresariales
Clonación de Platzi Movies usando React y React Router
Clonación de React Router en Componentes React
Navegación Avanzada con React Router DOM 6
No tienes acceso a esta clase
¡Continúa aprendiendo! Únete y comienza a potenciar tu carrera
Conoce cómo llevar la aplicación Platzi Movies al siguiente nivel utilizando React.js. La propuesta es transformar una aplicación ya existente, construida previamente con JavaScript en su versión Vanilla, en una moderna aplicación basada en React. Este paso no solo mejorará tu habilidad técnica, sino que también te permitirá comprender conceptos de navegación avanzada con JavaScript moderno. Prepárate para aprovechar las ventajas de React y React Router, dos herramientas fundamentales en el desarrollo web actual.
Para transformar efectivamente Platzi Movies usando React, es esencial mantener las características existentes de la aplicación. Estas incluyen:
Navegación Intuitiva: La aplicación debe permitir navegar de manera fluida entre distintos componentes, aún más integrada gracias a React Router
. Esto implica utilizar rutas para presentar diferentes secciones como películas en tendencias, búsquedas y filtros por categorías.
Rutas Personalizadas: Anteriormente, el proyecto utilizaba un sistema de routing artesanal basado en hash
. Ahora, es tu misión replicar estas rutas utilizando las poderosas capacidades de React Router para ofrecer una experiencia de usuario optimizada.
Infinite Scrolling: Este comportamiento, ya presente en la versión original, permite cargar más contenido a medida que el usuario se desplaza, mejorando la interacción y la carga dinámica de datos.
React Router es una librería esencial para gestionar las rutas de tu aplicación React. Al usarla, podrás controlar fácilmente la navegación entre componentes sin recargar la página, mejorando la eficiencia y ofreciendo una experiencia más moderna. He aquí un ejemplo básico de cómo configurar y utilizar React Router:
import React from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import HomePage from './components/HomePage';
import MoviesTrending from './components/MoviesTrending';
import SearchPage from './components/SearchPage';
import CategoryFilter from './components/CategoryFilter';
function App() {
return (
<Router>
<Switch>
<Route path="/" exact component={HomePage} />
<Route path="/trending" component={MoviesTrending} />
<Route path="/search" component={SearchPage} />
<Route path="/categories" component={CategoryFilter} />
</Switch>
</Router>
);
}
export default App;
Modulariza tu Código: Una de las mejores prácticas en React es dividir tu aplicación en componentes reutilizables. Esto no solo reduce la complejidad del código, sino que también facilita la escalabilidad del proyecto.
Mantén un Estado Global: Considera el uso de contextos o soluciones como Redux para mantener el estado global de tu aplicación, especialmente si manejas datos compartidos entre diversos componentes.
Despliega tu Aplicación: Una vez terminado el clon, es recomendable desplegar la aplicación. Puedes elegir plataformas como Vercel o Netlify. Publicar tu proyecto no solo aumentará su visibilidad, sino también proporcionará un valioso portafolio para futuras oportunidades.
Desarrollar una aplicación moderna utilizando React representa un avance significativo en tu trayectoria como desarrollador web. Este proyecto te permitirá practicar con herramientas y técnicas de última generación, preparando tu perfil profesional para los desafíos actuales. Comparte tus avances, ten presente que el aprendizaje continuo es clave para el éxito en el mundo del desarrollo. ¡Adelante, el límite lo pones tú!
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?