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:
✅ Consume datos reales de la API de TMDb
✅ Muestra imágenes de pósters correctamente concatenadas
✅ Tiene manejo de errores para imágenes faltantes
✅ Interfaz atractiva con efectos hover y badges
✅ Diseño responsive que funciona en todos los dispositivos
Consejos adicionales:
Optimización de imágenes: Usa tamaños apropiados según el dispositivo
Lazy loading: El atributo loading="lazy" mejora el rendimiento
Fallbacks: Siempre proporciona imágenes alternativas cuando falten
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
Publicar un comentario