The best comment " Esa película es horrible" jajaja
Presentación del proyecto: PlatziMovies
TheMovieDB: análisis de su API
Bocetos en papel y diseño en Figma
Configuración inicial y maquetación del proyecto
Configuración del entorno de desarrollo
Maquetación del proyecto: HTML y CSS
Consumiendo la API
Lista de películas en tendencia
Lista de categorías
Migración a Axios
Navegación
Location y hash navigation
Mostrando y ocultando secciones
Error: carga duplicada de datos
Views
Filtrando películas por categoría
Retos: scrollTop y DRY
Buscador de películas
Retos: historial de navegación y página de tendencias
Endpoint de detalles de una película
Lista de películas recomendadas
Próximos pasos
Toma el Curso Profesional de Consumo de API REST con JavaScript
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. Aprovecha el precio especial.
Antes: $249
Paga en 4 cuotas sin intereses
Termina en:
Juan David Castro Gallego
Aportes 43
Preguntas 7
The best comment " Esa película es horrible" jajaja
¿Qué opinan de esta solución?
.
No sé si más adelante lo corrige el profe, pero al momento de entrar en una categoría que contiene un espacio en su título nos encontramos esto:
Pero pues lo podemos solucionar con un simple replace:p
Para que la página inicie arriba y no en el footer, agregar en la función categoriesPage() el siguiente código:
window.scroll(0,0);
Para solucionar el segundo reto (ahorrarnos líneas de código para una funcionalidad que se repite), lo que hice fue hacer una función llamada printMoviePosters, que recibe como paråmetros el objeto movies, y la sección en la que corresponde. Luego, cada función que la necesite la llamará, pasándole los parámetros correspondientes:
const printMoviePosters = (movies, section) => {
section.innerHTML = "";
movies.forEach(movie => {
const movieContainer = document.createElement('div');
movieContainer.classList.add('movie-container');
const movieImg = document.createElement('img');
movieImg.classList.add('movie-img');
movieImg.setAttribute('alt', movie.title);
movieImg.setAttribute(
'src',
`${POSTER_300W_URL}${movie.poster_path}`
);
movieContainer.appendChild(movieImg);
section.appendChild(movieContainer);
});
}
const getTrendingMoviesPreview = async () => {
const { data } = await api('/trending/movie/day');
const movies = data.results;
printMoviePosters(movies, trendingMoviesPreviewList);
}
const getMoviesByCategory = async (categoryId) => {
const { data } = await api('/discover/movie', {
params: {
with_genres: categoryId,
},
}
);
const movies = data.results;
printMoviePosters(movies, genericSection);
}
En cuanto al reto del problema del scroll, creo haberlo solucionado agregando el scrollTo(0,0) en la función que se dispara al hacer click en el botón categoryTitle:
categoryTitle.addEventListener('click', () => {
location.hash = `#category=${category.id}-${category.name}`;
window.scrollTo(0, 0);
})
Se me ocurrió como solución alternativa para mostrar la categoría al usuario, colocarla como parte del titulo
document.title=`PlatziMovies ${category.name}`;
Que les parece?
Esta fue mi solución para re utilizar el código de las películas:
async function getTrendingMoviesPreview() {
body.scrollTop = 0
const { data } = await api(URL_TrendingMovies)
const movies = data.results
trendingMoviesPreviewList.innerHTML = ''
renderMovies(trendingMoviesPreviewList, movies)
}
async function getMoviesByCategory(id) {
const { data } = await api(URL_DiscoverMovie,{
params: {
with_genres: id
}
})
const movies = data.results
genericSection.innerHTML = ''
renderMovies(genericSection, movies)
}
function renderMovies(mainSection, movies) {
movies.forEach(movie => {
const movieContainer = document.createElement('div')
movieContainer.classList.add('movie-container')
const movieImg = document.createElement('img')
movieImg.classList.add('movie-img')
movieImg.setAttribute('alt', movie.title)
movieImg.setAttribute('src', `https://image.tmdb.org/t/p/w300${movie.poster_path}`)
movieContainer.appendChild(movieImg)
mainSection.appendChild(movieContainer)
})
}
También corregí el detalle de los espacios en el títulos de las categorías:
function categoriesPage() {
// console.log('Category!!')
headerSection.classList.remove('header-container--long')
headerSection.style.background = ''
arrowBtn.classList.remove('inactive')
arrowBtn.classList.remove('header-arrow--white')
headerTitle.classList.add('inactive')
headerCategoryTitle.classList.remove('inactive')
searchForm.classList.add('inactive')
trendingPreviewSection.classList.add('inactive')
categoriesPreviewSection.classList.add('inactive')
genericSection.classList.remove('inactive')
movieDetailSection.classList.add('inactive')
const [_ , categoryData] = location.hash.split('=')
const [categoryId, categoryName] = categoryData.split('-')
headerCategoryTitle.innerText = categoryName.replace('_', ' ')
getMoviesByCategory(categoryId)
}
![](
No logré lo del scroll 😫😭
Investigue el scrollTo(cord-x, cord-y);
con este método solucionamos el problema del scroll que aparece hasta abajo, solo debemos insertar el código en la función de navegación de categoriesPage:
window.scrollTo(0,0);
de este modo se ubicara al inicio de la pagina.
Hay otro detalle que cuando hacemos un refresh (f5), o usamos el botón de ir a atras en el navegador, si tenemos el scroll hasta abajo este no cambia su posición.
Un comportamiento optimo seria que si actualizamos la pagina o le damos hacia atras o adelante, este se ubique al inicio de la pagina, para solucionar esto también podemos usar el scrollTo anclado a un evento.
window.onbeforeunload = () => {
scrollTo(0,0);
}
Este bloque de código, va en el main.js… ahora cada vez que actualicemos o le demos hacia atrás, la vista de la pagina se ubicara desde el inicio.
Para poner en español las respuestas de la api (Categorías y algunos posters de Series y Películas) usando Axios, definimos en los parametros de la solicitud el parametro ‘language’ : ‘es’.
.
const api = axios.create({
baseURL: 'https://api.themoviedb.org/3',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
params:{
'api_key': API_KEY,
'language': 'es' //para que las categorias lleguen en español
}
})
Para el reto les dejo la función que usé, por cursos anteriores y documentación que he leído, decían que no se recomienda usar innerHTML para manejo del DOM por cuestiones de seguridad y también que es menos eficiente, en mi caso hago uso de la función replaceChildren() que en caso de no pasarle ningún parámetro elimina los hijos de un contenedor
function createMovies(movies, container){
let moviesList = [];
container.replaceChildren();
movies.forEach( movie => {
const movieContainer = document.createElement('div');
const movieImg = document.createElement('img');
movieContainer.classList.add('movie-container');
movieImg.classList.add('movie-img');
movieImg.setAttribute('alt', movie.title);
movieImg.setAttribute('src', 'https://image.tmdb.org/t/p/w300/'+movie.poster_path);
movieContainer.append(movieImg);
moviesList.push(movieContainer);
});
container.append(...moviesList);
}
Para evitar que Ponga en el Title los caracteres codificados % de espacio y acentuación se puede usar el:
decodeURIComponent()
en el innerHTML
es decir: en el navigation.js
const [categ, categoryData] = location.hash.split('=');// ['#category','id-name']
const [categoryId, categoryName] = categoryData.split('-');
headerCategoryTitle.innerHTML= decodeURIComponent(categoryName);
getMoviesByCategory(categoryId);
Y para los que han usado la funcion insertAdjacentHTML en el main.
esta es una propuesta que funciona para enviar el hash del evento click:
categories.forEach(category => {
const categoryId = category.id;
const categoryName = category.name;
//const encodedCategoryName = encodeURIComponent(categoryName);
const hash = `#category=${categoryId}-${categoryName}`;
categoriesPreviewList.insertAdjacentHTML('beforeend', `
<div class="category-container">
<h3 id="id${categoryId}" class="category-title">${categoryName}</h3>
</div>
`);
const categoryTitle = document.querySelector(`#id${categoryId}`);
categoryTitle.addEventListener('click', () => {
location.hash = hash;
});
});
Like si antes de llegar a esta clase ya habías desarrollado esta funcionalidad jajajaja.
Para evitarnos escribir el mismo código que renderice cada película (ya sea en el home o en la lista de películas por categoría), podríamos crear una función al cual se le pase dos parámetros, la data y el nodo donde se deben ‘pintar’.
.
Puede ser algo más o menos así:
.
function renderMoviesHtml(data, node){
data.forEach(movie => {
const movieContainer = document.createElement('div');
movieContainer.classList.add('movie-container');
const movieImg = document.createElement('img');
movieImg.classList.add('movie-img');
movieImg.setAttribute('alt', movie.title);
movieImg.setAttribute('src', `https://image.tmdb.org/t/p/w300/${movie.poster_path}`);
movieContainer.appendChild(movieImg);
node.appendChild(movieContainer)
});
}
.
Luego sería llamar esta función pasándole los respectivos argumentos y ahorramos varias líneas de código.
Una clse genial!!
Creo que, para los que van comenzando pueden volver a repasae éste curso (proyecto) para un curso como “webpack”. Además, como el de “Single Page Application”.
Esta era mi solución:
const index = window.location.hash.indexOf('=')
const index2 = window.location.hash.indexOf('-')
const query = window.location.hash.substring(index + 1, index2)
es muy forzada
Solución al reto de cargar la página desde el top: basta con poner esta línea de código en nuestra función:
window.scrollTo(0, 0);
Ejemplo:
Y para el que quiera profundizar más, esta es su documentación:
https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollTop
Saludos! 😄
Buenas!
Puede que a alguien (que no este usando Axios) le suceda lo mismo que yo…
A la hora de hacer de ejecutar el codigo nos sale un Error 401 por consola en la funcion getMoviesByCategory() mas precisamente en el fetch()…
Esto se debe a que estamos pasando los Query Parameters o los EndPoints mal…
Esta es la URL a la que debemos hacer la peticion:
https://api.themoviedb.org/3/discover/movie?api_key=${API_KEY}&with_genres=${categoryID}
y es importante pasar primero la “API KEY” sino de otra forma no va a autenticarar la petición…
<code>
async function getCategoriesPreview() {
// solicitud a trending de peliculas para el home
// const res = await fetch(
// "https://api.themoviedb.org/3/genre/movie/list?api_key=" + API_KEY
// );
const { data } = await api("genre/movie/list");
const categories = data.genres;
// console.log({ categories, data });
categoriesPreviewList.innerHTML = "";
categories.forEach((category) => {
const categoriesPreviewList = document.querySelector(
"#categoriesPreview .categoriesPreview-list"
); // lo que hago aca es tomar el elemento que esta dentro del id trendingpreview y que contiene trendingpreview movielist
const categoryContainer = document.createElement("div");
categoryContainer.classList.add("category-container");
const categoryTitle = document.createElement("h3"); // estoy creando un nuevo elemento dentro de hatml (IMG)
categoryTitle.classList.add("category-title"); // estoy añadiendole una clase a la imagen
categoryTitle.setAttribute("id", "id" + category.id); // estoy añadiendo el atributo alt y su nombre invocando movie.title ( titulo que me envia la api)
categoryTitle.addEventListener("click", () => {
location.hash = `#category=${category.id}-${category.name}`;
});
const categoryTitleText = document.createTextNode(category.name);
categoryContainer.scrollTop; // asigno scrollTop al elemento que me direccionaré
categoryTitle.appendChild(categoryTitleText);
categoryContainer.appendChild(categoryTitle); // coloco dentro del movie container el movieImg
categoriesPreviewList.appendChild(categoryContainer);
});
Mi solución a uno de los retos
Me quise adelantar a la clase y hice el filtro por las peliculas en tendencia jajaja F
const filterCategoriesMovies = async (category) => {
const {data} = await API.get('trending/movie/day')
const movies = data.results
const filterMovies = movies.filter(movie => movie.genre_ids.includes(category))
genericSection.innerHTML = ""
filterMovies.forEach(movie => {
const movieContainer = document.createElement('div')
movieContainer.classList.add('movie-container')
const img = document.createElement('img')
img.src = `https://image.tmdb.org/t/p/w300/${movie.poster_path}`
img.title = movie.original_title
img.alt = movie.original_title
img.classList.add('movie-img')
movieContainer.appendChild(img)
genericSection.appendChild(movieContainer)
})
headerCategoryTitle.style.after = filterMovies.length
}
const getCategories = async () => {
const {data} = await API.get('genre/movie/list')
const categories = data.genres
categoriesPreviewList.innerHTML = ""
categories.forEach(category => {
const categoryName = document.createElement('h3')
categoryName.classList.add('category-title')
categoryName.textContent = category.name
categoryName.id = `id${category.id}`
categoryName.onclick = () => {
location.hash = `category=${category.id}-${category.name}`
headerCategoryTitle.textContent = `Results for ${category.name.toLowerCase()} movies`
filterCategoriesMovies(category.id)
}
categoryContainer.appendChild(categoryName)
categoriesPreviewList.appendChild(categoryContainer)
})
}
Si su proyecto esta en español lo más probable es que cuando incluyan las categorías en su URI esta se codifique en Unicode para evitar caracteres y signos especiales como las tildes. Pueden utilizar los métodos …
encode/decodeURIComponent()
para no perder dichos caracteres.
Ejemplo:
async function getCategoriesPreview() {
const { data } = await api('genre/movie/list');
const categories = data.genres;
categoriesPreviewList.innerHTML = "";
categories.forEach(category => {
const categoryContainer = document.createElement('div');
categoryContainer.classList.add('category-container');
const categoryTitle = document.createElement('h3');
categoryTitle.classList.add('category-title');
categoryTitle.setAttribute('id', `id${category.id}`);
categoryTitle.addEventListener('click', ()=>{
location.hash = encodeURIComponent(`category=${category.id}-${category.name}`);
})
const categoryTitleText = document.createTextNode(category.name);
categoryTitle.appendChild(categoryTitleText);
categoryContainer.appendChild(categoryTitle);
categoriesPreviewList.appendChild(categoryContainer);
});
}
function categoriesPage() {
console.log('categories!!');
headerSection.classList.remove('header-container--long');
headerSection.style.background = '';
arrowBtn.classList.remove('inactive');
arrowBtn.classList.remove('header-arrow--white');
headerTitle.classList.add('inactive');
headerCategoryTitle.classList.remove('inactive');
searchForm.classList.add('inactive');
trendingPreviewSection.classList.add('inactive');
categoriesPreviewSection.classList.add('inactive');
genericSection.classList.remove('inactive');
movieDetailSection.classList.add('inactive');
const [_,categoryData] = decodeURIComponent(location.hash).split('=');
const [categoryId, categoryName]=categoryData.split('-');
console.log(categoryName)
headerCategoryTitle.innerHTML=categoryName;
getMoviesByCategory(categoryId)
}
const [__,idOk]=categoryId.split(’%20’);
getMoviesByCategory(idOk,categoryName)
Para no seleccionar el %20 de la url
Para conseguir el aporte esta fue mi solución:
Mi solucion:
Hice una funcion con el codigo html y sus respectivos argumentos para añadirle
function appendHTML(data, container, containerClass, optionalClass = ""){
data.forEach(element=>{
container.innerHTML +=
`
<div class=${containerClass}>
<img src=${URL_IMG_POSTER + element.poster_path} alt=${element.title} class="img-film" width=100%>
<h3 class="movie-title ${optionalClass}">${element.title}</h3>
<p class="movieDetail-score">${element.vote_average}</p></div>
`
})
}
Si tienen el problema de que en la URL aparece “%20” pueden usar la siguiente expresión regular para transformar espacios en guiones
str = str.replace(/ /g, '-')`
Dejo esto por acá porque seguro que a alguien le va a servir… “Genre” es una de esas palabras en inglés que tiene una pronunciación muy rara… viene siendo algo como: “shonra”.
En google translate pueden darle al iconito de sonido para escuchar como se dice:
https://translate.google.com/?hl=es&sl=en&tl=es&text=genre&op=translate
Antes de ver la solucion de Juan hice mi propia solucion para conseguir el id del string hice esta linea de codigo
const catId = hash.split('')
.filter((n) => {
return Number(n)
})
.join('')
Importantisimo en pensar en el mantenimiento futuro, a mayor abstración de código, se hace más fácil el mantenimiento.
Este proyecto se está poniendo interesante. Yo nunca encuentro una película para ver. Ahora voy a poder buscar en este proyecto 😄
solución para obtener el id del location.hash.
let arrayHash = location.hash.split(’-’)[0];
let id = arrayStringHash.split(’=’)[1];
console.log(id)
Otra manera de obtener el ID del hash es de la siguiente manera:
.
const [hash, nameCategory] = location.hash.replace('#category=', '').split('-');
Supongo que justo ese método de “colocar el id seguido de un -
para luego colocar el nombre de la sección” es lo que utilizan aquí en platzi, je je. De ahí que las url de los cursos y las clases sean cosas como “/platzi.com/clases/2986-api-practico/48456-filtrando-peliculas-por-categoria/”.
Esta es mi solución usando templates en HTML, me parece que se optimiza más el código.
const getMoviesByCategory = async (id) =>{
try {
let options={
params: {
with_genres: id,
}
}
const {data} = await api('discover/movie', options),
movies = data.results;
console.log(movies);
movies.forEach(el => {
$template_categoriesPreview.querySelector('.movie_img').src = `https://image.tmdb.org/t/p/w300${el.poster_path}`;
let $clone = d.importNode($template_categoriesPreview, true);
$fragment.appendChild($clone);
});
$genericSection.innerHTML ="",
$genericSection.appendChild($fragment);
} catch (error) {
console.log(error);
}
}
Esta es la solucion de uno de los retos, para que la pagina se cargue de primeras.
Solo se coloca esta linea en la función de categorías en el navigation.js
window.scrollTo(0, 0);
Y para el reto de encapsular código repetido, supongo que se hace una función mas que nada solo para la petición a la api no?, ya que en mi caso las secciones tienen una disposición diferente por ello la manipulacion del DOM de c/u es totalmente diferente
La solución del profe en la unión del id con el nombre de la categoría se conoce como un slug
de dos variables, id and name.
.
La solución que yo agregado para tener los id y el name de la categoría en cada nodo es agregando un dataset la adición del atributo ‘data-namedataset’, esto es para hacer que el contenedor agreguemos un solo evento y aplciar la delegación de eventos detectando el target que sería en este caso el h3.
.
Es recomendable hacerlo así para optimizar carga de eventos y no hacerlo en cada loop.
getNode.categoriesPreviewList.addEventListener('click', (e) => {
const target = e.target;
if (target && target.nodeName === 'H3') {
const categoryContainerNode = target.parentNode;
const categoryID = categoryContainerNode.dataset.categoryid;
const categoryName = categoryContainerNode.dataset.categoryname.toLowerCase();
window.location.hash = `category=${categoryID}-${categoryName}`;
}
});
En los cursos al inicio de la escuela, los profesores decian: Si se repite, debe ser una función:
Para usar:
window.scrollTo : Desplaza el visor a un conjunto de coordenadas en el documento.
window.scrollTo(0, 0) : Para mover siempre al inicio.
decoreURI() : Sustituye a cada secuencia de escape codificado en URI con el carácter que representa.
headerCategoryTitle.innerHTML = decodeURI(categoryName);
La solución que propongo para el reto del código duplicado es:
.
.
.
append
en lugar de appendChild
, ya que así se pueden mandar múltiples nodos en vez de sólo uno; y se es más óptimo, ya que no se hacen tantos appends sino uno solo..
append
aplicada al contenedor que corresponda, ya que esa responsabilidad sigue perteneciendo a la que hace el llamado a la API..
¿Quieres ver más aportes, preguntas y respuestas de la comunidad?