40. 6 Concatenar Url imagen

 

Tutorial 40.6: Concatenar URL de imágenes para mostrar pósters

Introducción al manejo de imágenes desde la API

Ahora que tenemos nuestra aplicación obteniendo datos de películas, necesitamos mostrar las imágenes de los pósters. La API de TMDb nos proporciona las rutas de las imágenes, pero necesitamos concatenarlas correctamente con la URL base de imágenes.

Paso 1: Entender la estructura de datos de la API

Cuando consumimos la API, obtenemos objetos con esta estructura:

javascript

{

  "id": 12345,

  "title": "Nombre de la Película",

  "poster_path": "/ruta/imagen.jpg",

  "backdrop_path": "/ruta/fondo.jpg",

  "overview": "Descripción de la película...",

  "vote_average": 7.5,

  "release_date": "2023-05-15",

  // ... más propiedades

}

Importante: poster_path contiene solo la parte final de la ruta, necesitamos concatenarla con la URL base.

Paso 2: Configurar ContextMovieCard para obtener datos

ContextMovieCard.jsx actualizado

jsx

import { useEffect, useState } from "react";

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

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

import "./ContextMovieCard.css";


export function ContextMovieCard() {

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

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

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


    useEffect(() => {

        // Función asíncrona para obtener las películas

        const fetchMovies = async () => {

            try {

                setLoading(true);

                // Consumir la API con el endpoint discover/movie

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

                setMovies(data.results);

                console.log("Datos obtenidos:", data); // Para depuración

            } catch (err) {

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

                console.error("Error en fetchMovies:", err);

            } finally {

                setLoading(false);

            }

        };


        fetchMovies();

    }, []); // Array vacío significa que se ejecuta solo una vez


    if (loading) {

        return (

            <div className="loading">

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

            </div>

        );

    }


    if (error) {

        return (

            <div className="error">

                <p>{error}</p>

            </div>

        );

    }


    return (

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

            <h2 className="section-title">Películas Populares</h2>

            

            <div className="movies-grid">

                {movies.map((movie) => (

                    <MovieCard 

                        key={movie.id} 

                        movie={movie} 

                    />

                ))}

            </div>

        </div>

    );

}

Paso 3: Configurar MovieCard para mostrar imágenes

MovieCard.jsx completo

jsx

import "./MovieCard.css";


export function MovieCard({ movie }) {

    // Verificar si la película tiene imagen

    const hasPoster = movie.poster_path !== null;

    

    // URL base para imágenes de TMDb

    const imageBaseUrl = "https://image.tmdb.org/t/p/";

    

    // Tamaño de imagen (w300 = 300px de ancho)

    const imageSize = "w300";

    

    // Concatenar URL completa de la imagen

    const posterUrl = hasPoster 

        ? `${imageBaseUrl}${imageSize}${movie.poster_path}`

        : "https://via.placeholder.com/300x450/16213e/ffffff?text=No+Poster";


    return (

        <div className="movie-card">

            <div className="poster-container">

                <img

                    src={posterUrl}

                    alt={`Póster de ${movie.title}`}

                    className="movie-poster"

                    loading="lazy" // Optimización: carga perezosa

                />

                

                {/* Badge de calificación */}

                <div className="rating-badge">

                    ⭐ {movie.vote_average.toFixed(1)}

                </div>

            </div>

            

            <div className="movie-info">

                <h3 className="movie-title">{movie.title}</h3>

                

                <div className="movie-meta">

                    <span className="release-date">

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

                    </span>

                    <span className="language">

                        {movie.original_language.toUpperCase()}

                    </span>

                </div>

                

                <p className="movie-overview">

                    {movie.overview.substring(0, 100)}...

                </p>

            </div>

        </div>

    );

}

Paso 4: Explicación detallada de la concatenación de URLs

Base URL de imágenes TMDb:

text

https://image.tmdb.org/t/p/

Tamaños disponibles:

  • w92 - 92px

  • w154 - 154px

  • w185 - 185px

  • w342 - 342px

  • w500 - 500px

  • w780 - 780px

  • original - Tamaño original

Ejemplo de concatenación:

javascript

// Datos de la API

const poster_path = "/abc123def456.jpg";


// Componentes

const baseUrl = "https://image.tmdb.org/t/p/";

const size = "w300";


// URL completa

const fullUrl = baseUrl + size + poster_path;

// Resultado: "https://image.tmdb.org/t/p/w300/abc123def456.jpg"

Paso 5: Mejorar MovieCard.css para imágenes

css

.movie-card {

    background: #16213e;

    border-radius: 12px;

    overflow: hidden;

    transition: all 0.3s ease;

    height: 100%;

    display: flex;

    flex-direction: column;

}


.movie-card:hover {

    transform: translateY(-10px);

    box-shadow: 0 15px 30px rgba(255, 215, 0, 0.15);

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

}


.poster-container {

    position: relative;

    height: 300px;

    overflow: hidden;

    background: #0f3460;

}


.movie-poster {

    width: 100%;

    height: 100%;

    object-fit: cover;

    transition: transform 0.5s ease;

}


.movie-card:hover .movie-poster {

    transform: scale(1.05);

}


.rating-badge {

    position: absolute;

    top: 10px;

    right: 10px;

    background: linear-gradient(135deg, #ffd700 0%, #ffed4e 100%);

    color: #000;

    padding: 6px 12px;

    border-radius: 20px;

    font-weight: bold;

    font-size: 0.9rem;

    display: flex;

    align-items: center;

    gap: 5px;

    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);

    z-index: 10;

}


.movie-info {

    padding: 1.2rem;

    flex-grow: 1;

    display: flex;

    flex-direction: column;

    gap: 0.8rem;

}


.movie-title {

    margin: 0;

    font-size: 1.1rem;

    color: #ffffff;

    font-weight: 600;

    line-height: 1.3;

    min-height: 2.8rem;

}


.movie-meta {

    display: flex;

    justify-content: space-between;

    align-items: center;

    font-size: 0.85rem;

}


.release-date {

    color: #a0a0a0;

    font-weight: 500;

}


.language {

    color: #ffd700;

    font-weight: bold;

    padding: 3px 10px;

    background: rgba(255, 215, 0, 0.1);

    border-radius: 6px;

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

}


.movie-overview {

    color: #b0b0b0;

    font-size: 0.9rem;

    line-height: 1.4;

    margin: 0;

    flex-grow: 1;

    display: -webkit-box;

    -webkit-line-clamp: 3;

    -webkit-box-orient: vertical;

    overflow: hidden;

}


/* Placeholder para imágenes faltantes */

.poster-container.no-image::before {

    content: "🎬";

    position: absolute;

    top: 50%;

    left: 50%;

    transform: translate(-50%, -50%);

    font-size: 3rem;

    color: rgba(255, 255, 255, 0.2);

}


/* Responsive */

@media (max-width: 768px) {

    .poster-container {

        height: 250px;

    }

    

    .movie-title {

        font-size: 1rem;

        min-height: 2.4rem;

    }

    

    .rating-badge {

        padding: 4px 10px;

        font-size: 0.8rem;

    }

}


@media (max-width: 480px) {

    .poster-container {

        height: 200px;

    }

    

    .movie-info {

        padding: 1rem;

        gap: 0.6rem;

    }

    

    .movie-overview {

        font-size: 0.8rem;

        -webkit-line-clamp: 2;

    }

}

Paso 6: Mejorar ContextMovieCard.css

css

.context-movie-card {

    padding: 2rem 0;

}


.section-title {

    text-align: center;

    color: #ffffff;

    font-size: 2.2rem;

    margin-bottom: 2rem;

    position: relative;

    padding-bottom: 1rem;

}


.section-title::after {

    content: "";

    position: absolute;

    bottom: 0;

    left: 50%;

    transform: translateX(-50%);

    width: 100px;

    height: 3px;

    background: linear-gradient(90deg, transparent, #ffd700, transparent);

}


.movies-grid {

    display: grid;

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

    gap: 2rem;

    padding: 0 1rem;

}


.loading {

    display: flex;

    flex-direction: column;

    align-items: center;

    justify-content: center;

    min-height: 300px;

    gap: 1.5rem;

}


.loading::before {

    content: "";

    width: 60px;

    height: 60px;

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

    border-top: 5px solid #ffd700;

    border-radius: 50%;

    animation: spin 1s linear infinite;

}


@keyframes spin {

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

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

}


.loading p {

    color: #ffd700;

    font-size: 1.2rem;

    font-weight: 500;

}


.error {

    text-align: center;

    padding: 3rem;

    background: rgba(255, 107, 107, 0.1);

    border-radius: 10px;

    border: 1px solid rgba(255, 107, 107, 0.3);

}


.error p {

    color: #ff6b6b;

    font-size: 1.2rem;

    margin-bottom: 1rem;

}


.retry-button {

    padding: 0.6rem 1.5rem;

    background: linear-gradient(135deg, #ff6b6b 0%, #ff8e8e 100%);

    border: none;

    border-radius: 6px;

    color: white;

    font-weight: bold;

    cursor: pointer;

    transition: all 0.3s ease;

}


.retry-button:hover {

    transform: translateY(-2px);

    box-shadow: 0 5px 15px rgba(255, 107, 107, 0.3);

}


/* Responsive */

@media (max-width: 1024px) {

    .movies-grid {

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

        gap: 1.5rem;

    }

}


@media (max-width: 768px) {

    .section-title {

        font-size: 1.8rem;

        margin-bottom: 1.5rem;

    }

    

    .movies-grid {

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

        gap: 1.2rem;

    }

}


@media (max-width: 480px) {

    .movies-grid {

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

        gap: 1rem;

    }

    

    .section-title {

        font-size: 1.5rem;

    }

}

Paso 7: Función utilitaria para imágenes (opcional)

Crea un archivo src/utils/imageUtils.js:

javascript

/**

 * Utilidades para manejar imágenes de TMDb

 */


const IMAGE_BASE_URL = "https://image.tmdb.org/t/p/";


/**

 * Genera la URL completa para una imagen de TMDb

 * @param {string} path - Ruta de la imagen (ej: "/abc123.jpg")

 * @param {string} size - Tamaño deseado (default: "w300")

 * @returns {string} URL completa de la imagen

 */

export function getImageUrl(path, size = "w300") {

    if (!path) {

        return "https://via.placeholder.com/300x450/16213e/ffffff?text=No+Image";

    }

    return `${IMAGE_BASE_URL}${size}${path}`;

}


/**

 * Genera la URL para el backdrop de una película

 * @param {string} path - Ruta del backdrop

 * @param {string} size - Tamaño deseado (default: "w1280")

 * @returns {string} URL completa del backdrop

 */

export function getBackdropUrl(path, size = "w1280") {

    return getImageUrl(path, size);

}


/**

 * Genera la URL para el poster de una película

 * @param {string} path - Ruta del poster

 * @param {string} size - Tamaño deseado (default: "w500")

 * @returns {string} URL completa del poster

 */

export function getPosterUrl(path, size = "w500") {

    return getImageUrl(path, size);

}

Resultado final

¡Perfecto! Ahora tu aplicación:

  1. ✅ Consume datos reales de la API de TMDb

  2. ✅ Muestra imágenes de pósters correctamente concatenadas

  3. ✅ Tiene manejo de errores para imágenes faltantes

  4. ✅ Interfaz atractiva con efectos hover y badges

  5. ✅ Diseño responsive que funciona en todos los dispositivos

Consejos adicionales:

  1. Optimización de imágenes: Usa tamaños apropiados según el dispositivo

  2. Lazy loading: El atributo loading="lazy" mejora el rendimiento

  3. Fallbacks: Siempre proporciona imágenes alternativas cuando falten

  4. CDN: TMDb usa Cloudinary como CDN, lo que garantiza buen rendimiento

¡Excelente trabajo! Ahora tus películas tienen imágenes atractivas y profesionalmente presentadas.


Comentarios

Entradas más populares de este blog

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

29. useEffect

38. 4 Routers