38. 4 Routers

 

Tutorial 38.4: Implementando Router en nuestra App de Películas

Introducción a React Router

Ahora que tenemos nuestra aplicación mostrando películas, necesitamos implementar navegación entre diferentes vistas. Vamos a crear rutas para:

  • Ver la lista de películas (página principal)

  • Ver el detalle de cada película

  • Otras vistas futuras (búsqueda, favoritos, etc.)

Paso 1: Estructura de carpetas

Primero, vamos a organizar nuestro proyecto con una estructura más profesional:

text

src/

├── assets/

├── components/

│   ├── ContextMovieCard.jsx

│   └── MovieCard.jsx

├── data/

├── pages/

│   └── LandingPage.jsx

├── routers/

│   └── routes.jsx

├── App.css

├── App.jsx

├── index.css

└── main.jsx

Paso 2: Instalar React Router DOM

Abrimos una terminal en nuestro proyecto 1-peliculas y ejecutamos:

bash

cd 1-peliculas

yarn add react-router-dom

O con npm:

bash

npm install react-router-dom

Paso 3: Crear componentes básicos

MovieCard.jsx (Componente individual de película)

jsx

export function MovieCard() {

    return (

        <div>

            <h1>Movie Card</h1>

            {/* Aquí irá la información de cada película */}

        </div>

    );

}

ContextMovieCard.jsx (Contenedor de películas)

jsx

export function ContextMovieCard() {

    return (

        <div>

            <h1>Context Movie Card</h1>

            {/* Aquí irán múltiples MovieCard */}

        </div>

    );

}

Paso 4: Crear la Landing Page

LandingPage.jsx

jsx

import { ContextMovieCard } from "../components/ContextMovieCard";


export function LandingPage() {

    return (

        <div>

            <ContextMovieCard />

        </div>

    );

}

Paso 5: Configurar el Router

routes.jsx

jsx

import { BrowserRouter as Router, Routes, Route } from "react-router-dom";

import { LandingPage } from "../pages/LandingPage";


export function AppRoutes() {

    return (

        <Router>

            <Routes>

                {/* Ruta principal */}

                <Route path="/" element={<LandingPage />} />

                

                {/* Ruta para detalles de película */}

                <Route path="/movie/:id" element={<div>Detalle de Película</div>} />

                

                {/* Ruta 404 */}

                <Route path="*" element={<div>Página no encontrada</div>} />

            </Routes>

        </Router>

    );

}


export default AppRoutes;

Paso 6: Integrar el Router en App.jsx

App.jsx actualizado

jsx

import './App.css';

import AppRoutes from './routers/routes';


function App() {

    return (

        <div className="App">

            <header>

                <h1 className='title'>Películas 🎬</h1>

            </header>

            <main>

                <AppRoutes />

            </main>

        </div>

    );

}


export default App;

Paso 7: Mejorar los componentes con datos reales

Actualizando ContextMovieCard.jsx

jsx

import { useEffect, useState } from 'react';

import { get } from '../data/httpClient';

import { MovieCard } from './MovieCard';

import './ContextMovieCard.css';


export function ContextMovieCard() {

    const [movies, setMovies] = useState([]);

    const [loading, setLoading] = useState(true);

    const [error, setError] = useState(null);


    useEffect(() => {

        const fetchMovies = async () => {

            try {

                setLoading(true);

                const data = await get('discover/movie');

                setMovies(data.results.slice(0, 12)); // Limitar a 12 películas

            } catch (err) {

                setError('Error al cargar las películas');

                console.error(err);

            } finally {

                setLoading(false);

            }

        };


        fetchMovies();

    }, []);


    if (loading) {

        return (

            <div className="loading-container">

                <div className="spinner"></div>

                <p>Cargando películas...</p>

            </div>

        );

    }


    if (error) {

        return (

            <div className="error-container">

                <p>{error}</p>

                <button onClick={() => window.location.reload()}>

                    Reintentar

                </button>

            </div>

        );

    }


    return (

        <div className="context-movie-card">

            <div className="movies-grid">

                {movies.map((movie) => (

                    <MovieCard 

                        key={movie.id} 

                        movie={movie} 

                    />

                ))}

            </div>

        </div>

    );

}

Actualizando MovieCard.jsx

jsx

import { Link } from 'react-router-dom';

import './MovieCard.css';


export function MovieCard({ movie }) {

    return (

        <Link to={`/movie/${movie.id}`} className="movie-card-link">

            <div className="movie-card">

                <img

                    src={`https://image.tmdb.org/t/p/w300${movie.poster_path}`}

                    alt={movie.title}

                    className="movie-poster"

                />

                <div className="movie-info">

                    <h3>{movie.title}</h3>

                    <div className="movie-details">

                        <span className="rating">

                            ⭐ {movie.vote_average.toFixed(1)}

                        </span>

                        <span className="year">

                            {new Date(movie.release_date).getFullYear()}

                        </span>

                    </div>

                </div>

            </div>

        </Link>

    );

}

Paso 8: Crear archivos CSS para los componentes

ContextMovieCard.css

css

.context-movie-card {

    padding: 2rem 0;

}


.movies-grid {

    display: grid;

    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));

    gap: 2rem;

    padding: 0 1rem;

}


.loading-container {

    display: flex;

    flex-direction: column;

    align-items: center;

    justify-content: center;

    min-height: 300px;

    gap: 1rem;

}


.spinner {

    width: 50px;

    height: 50px;

    border: 5px solid rgba(255, 215, 0, 0.3);

    border-top: 5px solid #ffd700;

    border-radius: 50%;

    animation: spin 1s linear infinite;

}


@keyframes spin {

    0% { transform: rotate(0deg); }

    100% { transform: rotate(360deg); }

}


.error-container {

    text-align: center;

    padding: 3rem;

    color: #ff6b6b;

}


.error-container button {

    margin-top: 1rem;

    padding: 0.5rem 1rem;

    background: #ffd700;

    border: none;

    border-radius: 5px;

    color: #000;

    cursor: pointer;

    font-weight: bold;

}


.error-container button:hover {

    background: #ffed4e;

}

MovieCard.css

css

.movie-card-link {

    text-decoration: none;

    color: inherit;

}


.movie-card {

    background: #16213e;

    border-radius: 10px;

    overflow: hidden;

    transition: transform 0.3s ease, box-shadow 0.3s ease;

    height: 100%;

}


.movie-card:hover {

    transform: translateY(-10px);

    box-shadow: 0 15px 30px rgba(0, 0, 0, 0.4);

}


.movie-poster {

    width: 100%;

    height: 300px;

    object-fit: cover;

}


.movie-info {

    padding: 1rem;

}


.movie-info h3 {

    margin: 0 0 0.5rem 0;

    font-size: 1rem;

    white-space: nowrap;

    overflow: hidden;

    text-overflow: ellipsis;

    color: #ffffff;

}


.movie-details {

    display: flex;

    justify-content: space-between;

    align-items: center;

}


.rating {

    color: #ffd700;

    font-weight: bold;

    display: flex;

    align-items: center;

    gap: 0.3rem;

}


.year {

    color: #a0a0a0;

    font-size: 0.9rem;

}


/* Responsive */

@media (max-width: 768px) {

    .movies-grid {

        grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));

        gap: 1rem;

    }

    

    .movie-poster {

        height: 225px;

    }

}

Paso 9: Actualizar App.css para incluir el main

css

/* Estilos existentes... */


main {

    min-height: calc(100vh - 200px);

    padding-bottom: 3rem;

}


/* Estilos para el título */

.title {

    text-align: center;

    font-size: 3rem;

    margin: 2rem 0;

    color: #ffd700;

    text-shadow: 0 0 10px rgba(255, 215, 0, 0.3);

}


/* Estilos para el header */

header {

    padding: 1rem;

    background: linear-gradient(135deg, #16213e 0%, #0f3460 100%);

    border-bottom: 3px solid #ffd700;

    margin-bottom: 2rem;

}

Paso 10: Crear página de detalle de película

Crear MovieDetailPage.jsx

jsx

import { useParams } from 'react-router-dom';

import { useEffect, useState } from 'react';

import { get } from '../data/httpClient';

import './MovieDetailPage.css';


export function MovieDetailPage() {

    const { id } = useParams();

    const [movie, setMovie] = useState(null);

    const [loading, setLoading] = useState(true);

    const [error, setError] = useState(null);


    useEffect(() => {

        const fetchMovie = async () => {

            try {

                setLoading(true);

                const data = await get(`movie/${id}`);

                setMovie(data);

            } catch (err) {

                setError('Error al cargar los detalles de la película');

                console.error(err);

            } finally {

                setLoading(false);

            }

        };


        fetchMovie();

    }, [id]);


    if (loading) {

        return (

            <div className="loading">

                <p>Cargando detalles...</p>

            </div>

        );

    }


    if (error || !movie) {

        return (

            <div className="error">

                <p>{error || 'Película no encontrada'}</p>

            </div>

        );

    }


    return (

        <div className="movie-detail">

            <div className="movie-backdrop">

                <img

                    src={`https://image.tmdb.org/t/p/original${movie.backdrop_path}`}

                    alt={movie.title}

                />

                <div className="backdrop-overlay"></div>

            </div>

            

            <div className="movie-content">

                <div className="movie-poster">

                    <img

                        src={`https://image.tmdb.org/t/p/w500${movie.poster_path}`}

                        alt={movie.title}

                    />

                </div>

                

                <div className="movie-info">

                    <h1>{movie.title}</h1>

                    

                    <div className="movie-meta">

                        <span className="rating">

                            ⭐ {movie.vote_average.toFixed(1)}/10

                        </span>

                        <span className="runtime">

                            🕒 {movie.runtime} min

                        </span>

                        <span className="release-date">

                            📅 {new Date(movie.release_date).toLocaleDateString()}

                        </span>

                    </div>

                    

                    <div className="genres">

                        {movie.genres.map(genre => (

                            <span key={genre.id} className="genre-tag">

                                {genre.name}

                            </span>

                        ))}

                    </div>

                    

                    <div className="overview">

                        <h2>Sinopsis</h2>

                        <p>{movie.overview}</p>

                    </div>

                    

                    <div className="additional-info">

                        <div className="info-item">

                            <strong>Presupuesto:</strong>

                            <span>${movie.budget.toLocaleString()}</span>

                        </div>

                        <div className="info-item">

                            <strong>Ingresos:</strong>

                            <span>${movie.revenue.toLocaleString()}</span>

                        </div>

                    </div>

                </div>

            </div>

        </div>

    );

}

Actualizar routes.jsx para incluir la nueva página

jsx

import { BrowserRouter as Router, Routes, Route } from "react-router-dom";

import { LandingPage } from "../pages/LandingPage";

import { MovieDetailPage } from "../pages/MovieDetailPage";


export function AppRoutes() {

    return (

        <Router>

            <Routes>

                <Route path="/" element={<LandingPage />} />

                <Route path="/movie/:id" element={<MovieDetailPage />} />

                <Route path="*" element={<div>Página no encontrada</div>} />

            </Routes>

        </Router>

    );

}


export default AppRoutes;

Resultado final

¡Excelente! Ahora tienes una aplicación con:

  1. ✅ Router funcional con React Router DOM v6

  2. ✅ Dos rutas principales: / (lista de películas) y /movie/:id (detalle)

  3. ✅ Componentes reutilizables y bien organizados

  4. ✅ Navegación entre páginas con <Link>

  5. ✅ Manejo de estados de carga y error

  6. ✅ Diseño responsive y atractivo

Próximas mejoras posibles

  1. Agregar más rutas: Búsqueda, favoritos, categorías

  2. Implementar navegación: Menú de navegación global

  3. Mejorar UX: Loading skeletons, transiciones suaves

  4. Agregar funcionalidades: Búsqueda en tiempo real, filtros avanzados

Consejo: Siempre mantén tu estructura de carpetas organizada. Esto te ayudará mucho cuando tu aplicación crezca y tengas muchos componentes y páginas.

¡Ahora tu aplicación de películas tiene navegación profesional! 


Comentarios

Entradas más populares de este blog

11. Creando un proyecto react con create-react-app

29. useEffect