Esta aplicación está hecha con Angular (versión 12.2.8). Esta es una aplicación de representación del lado del servidor que usa node.js
y express
y busca títulos de películas. Esta aplicación de una sola página está alojada de forma gratuita en Heroku (plataforma de aplicaciones en la nube). Deberá crear una cuenta gratuita con theoviedb.org para participar con este tutorial. Las instrucciones de cómo obtener una clave API TMDB están a continuación.
Instalado automáticamente localmente por Angular CLI
ng new movie-search --skip-git
Would you like to add Angular routing? Yes
Which stylesheet format would you like to use? SCSS
La ventaja de hacer que este servidor de aplicaciones se presente es que la aplicación puede aparecer en la pantalla (UI) más rápido que una aplicación normal. Esto brinda a los usuarios la oportunidad de ver el diseño de la aplicación antes de que se vuelva completamente interactivo. También puede ayudar con SEO.
ng add @nguniversal/express-engine
server.ts
para servir y manejar los datos de esta aplicación.package.json
cambia line 12
a "serve:ssr": "node dist/server/main.js",
angular.json
cambie line 20
a "outputPath": "dist/browser",
y line 129
a "outputPath": "dist/server",
server.ts
cambia line 14
a const distFolder = join(process.cwd(), 'dist/browser');
npm run build:ssr
npm run serve:ssr
ng gc modules/home --module=app.module.ts
homeComponent
de forma predeterminada. //app-routing.module.ts
import { HomeComponent } from './modules/home/home.component' ;
const routes : Routes = [
{
path : '' ,
component : HomeComponent
} ,
{
path : '**' ,
component : HomeComponent
}
] ;
app.component.html
. <!-- app.component.html -->
< div class =" container " >
< router-outlet > </ router-outlet >
</ div >
Para esta aplicación, necesitaremos agregar el HttpClinetModule
, ReactiveFormsModule
y FormsModule
al archivo app.module.ts
y agregarlo a imports
. Esto hará que estos módulos estén disponibles a través de toda la aplicación. El módulo HTTP nos permitirá hacer llamadas al servidor. ReactiveFormsModule
nos ayudará a usar FormControl
en la entrada HTML y cuando el valor cambia (texto en la entrada de búsqueda) se enviará una solicitud API al servidor.
//app.module.ts
import { HttpClientModule } from "@angular/common/http" ;
import { ReactiveFormsModule , FormsModule } from "@angular/forms" ;
FormControl
en home.component.html
.async Observable
y una function
para disparar en ININIT. <!-- home.component.html -->
< div class =" row " >
< input type =" search " class =" form-control " placeholder =" search " [formControl] =" searchField " >
< span class =" search-title " > {{ (results | async)?.length ? 'Results' : 'Search' }} </ span >
</ div >
//home.component.ts
import { Component , OnInit } from '@angular/core' ;
import { FormControl } from "@angular/forms" ;
import { Observable } from 'rxjs' ;
import {
debounceTime ,
distinctUntilChanged ,
tap ,
switchMap
} from 'rxjs/operators' ;
import { DataService } from '../../services/data.service' ;
@ Component ( {
selector : 'app-home' ,
templateUrl : './home.component.html' ,
styleUrls : [ './home.component.scss' ]
} )
export class HomeComponent implements OnInit {
public loading : boolean = false ;
public results : Observable < any > ;
public searchField : FormControl ;
constructor ( private dataService : DataService ) { }
ngOnInit ( ) : void {
this . searchField = new FormControl ( ) ;
this . results = this . searchField . valueChanges . pipe (
debounceTime ( 400 ) ,
distinctUntilChanged ( ) ,
tap ( _ => {
this . loading = true ;
} ) ,
switchMap ( term => this . dataService . search ( term ) ) ,
tap ( _ => ( this . loading = false ) )
) ;
}
}
Angular cli
en el terminal: ng gs services/data
server.ts
. //data.service.ts
import { Injectable } from '@angular/core' ;
import { HttpClient , HttpHeaders } from '@angular/common/http' ;
import { Observable } from 'rxjs' ;
const headers = new HttpHeaders ( ) . set ( 'Content-Type' , 'application/X-www-form-urlencoded' ) ;
@ Injectable ( {
providedIn : 'root'
} )
export class DataService {
public result : any ;
constructor ( private http : HttpClient ) { }
search ( item : string ) : Observable < any > {
let searchterm = `query= ${ item } ` ;
try {
this . result = this . http . post ( '/search' , searchterm , { headers } ) ;
return this . result ;
} catch ( e ) {
console . log ( e , 'error' )
}
}
}
En server.ts
crea una función para manejar la solicitud del cliente. Luego envíe la solicitud al punto final TMDB. https://api.themoviedb.org/3/search/movie?api_key=${apiKey}&language=en-US&page=1&include_adult=false&query=${term}
//server.ts line 35-40
server . post ( '/search' , async ( req , res ) => {
let searchquery = req . body . query ;
let encsearchquery = encodeURIComponent ( searchquery ) ;
const data = await api . data . search ( encsearchquery , apiKey ) ;
res . status ( 200 ) . json ( data ) ;
} )
api
y un archivo api.ts
mkdir api
luego cd api
luego touch api.ts
para configurar un directorio API.api
en el archivo server.ts
. import { api } from './api/api'
. En el futuro, si desea agregar diferentes solicitudes a la TMDB api
puede agregarlas a api.ts
para mantener el archivo server.ts
menos desordenado.
//api.ts
let request = require ( 'request' ) ;
let methods : any = { } ;
let searchInfo = [ ] ;
methods . search = async ( term : string , apiKey : string ) => {
let searchQuery = `https://api.themoviedb.org/3/search/movie?api_key= ${ apiKey } &language=en-US&page=1&include_adult=false&query= ${ term } ` ;
let searchPromise = new Promise ( ( resolve , reject ) => {
request ( searchQuery , { } , function ( err , res , body ) {
let data = JSON . parse ( body ) ;
searchInfo = data [ 'results' ] ;
resolve ( ) ;
} ) ;
} ) ;
let result = await searchPromise ;
return searchInfo ;
}
export const api = { data : methods } ;
Estoy usando la biblioteca request
para manejar la solicitud API y analizar la respuesta. En TypeScript puedo usar promesas para esperar a que la respuesta esté lista para evitar lanzar un error.
settings
.API
y envíe los detalles de su aplicación para recibir una clave API.Application Name: Movie Search
Application URL: localhost:4000
Application Summary: an app that will search for movies that are related to the search term entered into the app input and display them in the ui.
server.ts
en su aplicación.WARNING: Do not commit your api key to github. If you do it could be found and used by another party.
//server.ts
import 'zone.js/dist/zone-node' ;
import { ngExpressEngine } from '@nguniversal/express-engine' ;
import * as express from 'express' ;
import { join } from 'path' ;
import { enableProdMode } from '@angular/core' ;
import { AppServerModule } from './src/main.server' ;
import { APP_BASE_HREF } from '@angular/common' ;
import { existsSync } from 'fs' ;
import { api } from './api/api' ;
const bodyParser = require ( 'body-parser' ) ;
enableProdMode ( ) ;
// The Express app is exported so that it can be used by serverless Functions.
export function app ( ) : express . Express {
const server = express ( ) ;
const distFolder = join ( process . cwd ( ) , 'dist/browser' ) ;
const indexHtml = existsSync ( join ( distFolder , 'index.original.html' ) ) ? 'index.original.html' : 'index' ;
const apiKey = 'TMDB api key' ;
server . use ( bodyParser . urlencoded ( { extended : true } ) ) ;
...
Cuando recuperamos la respuesta de datos desde el punto final TMDB, se envía de regreso al cliente (front-end). El home.component.html
debe configurarse para mostrar un async Observable
.
<!-- home.component.html -->
< div class =" center-header " >
< h1 > Movie Search </ h1 >
< i > * This product uses the TMDb API but is not endorsed or certified by TMDb. </ i >
</ div >
< div class =" row " >
< input type =" search " class =" form-control " placeholder =" search " [formControl] =" searchField " >
< span class =" search-title " > {{ (results | async)?.length ? 'Results' : 'Search' }} </ span >
</ div >
< div class =" row wrapper " >
< div class =" no-res " *ngIf =" (results | async)?.length === 0 " > Nothing matches this search. </ div >
< div [ngClass] =" {'dn': item?.poster_path == null} " class =" col " *ngFor =" let item of results | async " >
< span class =" item " >
< span class =" bg " [ngStyle] =" {'background': 'linear-gradient(-225deg, rgba(0,0,0,0.5) 50%, rgba(0,0,0,0.5) 80%), url(https://image.tmdb.org/t/p/w440_and_h660_face'+ item?.poster_path +')' } " >
</ span >
</ span >
</ div >
</ div >
Hay algunas cosas para desempacar en esta interfaz de usuario. Estoy usando una condición ternaria dentro de un soporte de interpolación angular para mostrar la "búsqueda" o "resultados" de texto dependiendo si hay datos que mostrar.
{{ (results | async)?.length ? 'Results' : 'Search' }}
Estoy usando la directiva [ngClass]
que se separa del marco angular. Estoy agregando la clase dn
si los datos poster_path
son null
y luego en styles.scss
agregan .dn {display: none;}
para evitar elementos de película en blanco. También estoy usando la directiva [ngStyle]
para agregar la imagen de fondo de la imagen de póster de cada elemento de la película dinámicamente.
He agregado algunos estilos básicos de CSS para mostrar los resultados de la película en un diseño de columna Flex Fil. Esto también manejará pantallas móviles más pequeñas. Con un archivo scss
puede escribir CSS anidados como se muestra a continuación. Archivo SCSS completo para esta aplicación de búsqueda de películas
// styles.scss
html ,
body {
height : 100 % ;
font-family : Arial , sans-serif ;
margin : 0 ;
background-color : #303030 ;
}
.container {
color : #fff ;
min-height : 100 % ;
margin-bottom : -50 px ;
margin : 0 auto ;
max-width : 1380 px ;
.row {
display : flex ;
flex-direction : row ;
flex-wrap : wrap ;
width : 100 % ;
justify-content : center ;
.col {
display : flex ;
flex-direction : column ;
flex : 0 0 13 % ;
width : 100 % ;
color : #fff ;
margin-bottom : 5 px ;
position : relative ;
.item {
.bg {
background-size : cover !important ;
background-repeat : no-repeat !important ;
background-position : center !important ;
position : relative ;
height : 250 px ;
display : block ;
}
}
}
.col.dn {
display : none ;
}
}
.row.wrapper {
max-width : 1200 px ;
margin : 0 auto
}
}
@media ( max-width : 900 px ) {
.container .row .col {
flex : 0 0 20 % ;
}
}
@media ( max-width : 500 px ) {
.container .row .col {
flex : 0 0 33 % ;
}
}
Si desea alojar esta aplicación de forma gratuita, visite en cualquier momento que desee y comparta con otros, siga los pasos a continuación.
package.json
para Herokuline 6
Agregar "start:heroku": "node dist/server/main.js",
line 7
agregue "heroku-postbuild": "npm run build:ssr"
Procfile
a la raíz de esta aplicación.touch Procfile
Agregar esta línea web: npm run start:heroku
al archivo.process.env.TOKEN
a server.ts
antes de presionar a Github y Heroku.line 20
Agregar const apiKey = process.env.TOKEN;
git commit -am "make a commit."
Entonces git push
Heroku CLI
inicie sesión en Heroku desde Terminal Run: heroku login
.heroku create angular-movie-search
y git push heroku master
.key: TOKEN
y value: TMDB api key
.Si el nombre de la aplicación Heroku que creó está tomado, conforma un nombre único que está disponible. Agregaré una parte 2 para este tutrial para que podamos mostrar más datos de películas y hacer que la página interactiva cargando trailers de películas. Gracias por leer. Código fuente completo
angular cli
.Mostremos información sobre la película cuando pasamos la imagen de la película. La carga útil de búsqueda proporciona un puntaje de calificación de la película 0 - 10. La calificación (VOTO_AVERAGE) se puede convertir en una matriz para mostrar la calificación como íconos de estrella igual a la longitud de la matriz.
La payload
son los datos enviados después de hacer una solicitud de búsqueda a la api
. Obtendrá un máximo de 20 resultados por respuesta. Cuanto menor sea la carga útil, más rápido se producen los datos en la interfaz de usuario. Este es un ejemplo del primer objeto de un Oserverable enviado por la API.
{
"popularity" : 24.087 ,
"id" : 670466 ,
"video" : false ,
"vote_count" : 29 ,
"vote_average" : 6.8 ,
"title" : " My Valentine " ,
"release_date" : " 2020-02-07 " ,
"original_language" : " en " ,
"original_title" : " My Valentine " ,
"genre_ids" :[ 53 , 27 ],
"backdrop_path" : " /jNN5s79gjy4D3sJNxjQvymXPs9d.jpg " ,
"adult" : false ,
"overview" : " A pop singer's artistic identity is stolen by her ex-boyfriend/manager and shamelessly pasted onto his new girlfriend/protégé. Locked together late one night in a concert venue, the three reconcile emotional abuses of the past . . . until things turn violent. " , "poster_path" : " /mkRShxUNjeC8wzhUEJoFUUZ6gS8.jpg "
}
En la primera parte de este tutorial, utilicé poster_path
para mostrar la imagen de la película y ahora puedo usar el vote_average
para mostrar la calificación de la película. Creé una función dentro del controlador del componente para convertir la calificación en una matriz que luego puede representar el valor del vote_average
redondeado a un número entero y usar estrellas doradas para representar la calificación cuando me desplazé por la imagen de la película.
< span class =" star-rating " *ngFor =" let star of rating(item) " >
< span > ☆ </ span >
</ span >
//home.component.ts line 53
public rating ( movie ) {
let rating = Array ( Math . round ( movie . vote_average ) ) . fill ( 0 ) ;
movie . rating = rating ;
return rating ;
}
Luego diseñe el contenido para el valor devuelto de la calificación para que solo veamos las estrellas en el flotador. Archivo SCSS completo para el componente de diálogo
// components/dialog/dialog.component.scss
.item .bg .info {
background-color : rgba ( 0 , 0 , 0 , 0.0 );
position : absolute ;
bottom : 0 ;
color : #fff ;
font-size : 9 px ;
text-transform : uppercase ;
width : 100 % ;
transition : linear .3 s ;
p {
opacity : 0 ;
transition : linear .3 s ;
}
.star-rating {
color : transparent ;
span :before {
content : " 2605 " ;
position : absolute ;
color : gold ;
}
}
}
.item .bg .item :hover {
.info {
background-color : rgba ( 0 , 0 , 0 , 0.5 );
p {
color : #fff ;
opacity : 1 ;
font-size : 14 px ;
}
}
}
A continuación, voy a agregar un enlace que enviará otra solicitud de API para un avance de la película (vista previa) y luego abrirá un cuadro de diálogo (ventana emergente) para mostrar el avance con la descripción general (descripción) de la película.
Cuando obtengo la información del ensayador de la película de la API, quiero incrustar el enlace de los medios dentro de un HTML <iframe>
. Quiero agregar un enlace de "trailer" que abrirá una ventana para mostrar el trailer cuando se haga clic. Usando angular cli
generaré un nuevo componente de diálogo. En el tipo terminal ng gc components/dialog --module=app.module.ts
. Este comando agregará el componente a app.module.ts
automáticamente.
Para crear una ventana emergente desde cero, necesito usar un pequeño CSS y algunos trucos angulares para ayudarme a agregar una class
especial cuando se hace clic en el enlace "Trailer". El dialog component
utiliza un boolean
para agregar una clase active
a un div
para mostrar un fondo de superposición oscura con una ventana emergente colocada en el centro de la superposición. Usar una directiva angular [ngClass]
si isOpen
boolean
es true
Agregar clase active
a la id
de superposición. <div id="overlay" [ngClass]="{'active': isOpen}">
Esto me permite ocultar la superposición div
hasta que esté active
, cuando hago clic en el enlace del trailer y hago que isOpen
sea igual. Todo lo que necesito hacer es agregar algunas Inputs
al componente app-dialog
.
<!-- home.component.html line 1 -->
< app-dialog
[isOpen] =" isOpen "
[selectedMovie] =" selectedMovie "
[trailerUrl] =" trailerUrl "
(close) =" isOpen = false " > </ app-dialog >
//dialog.component.ts
import { Component , Input , Output , EventEmitter } from '@angular/core' ;
@ Component ( {
selector : 'app-dialog' ,
templateUrl : './dialog.component.html' ,
styleUrls : [ './dialog.component.scss' ]
} )
export class DialogComponent {
@ Input ( 'selectedMovie' )
public selectedMovie : any ;
@ Input ( 'trailerUrl' )
public trailerUrl : any ;
@ Input ( 'isOpen' )
public isOpen : boolean ;
@ Output ( ) close = new EventEmitter ( ) ;
constructor ( ) { }
public closeModal ( ) {
this . isOpen = false ;
this . selectedMovie = null ;
this . trailerUrl = null ;
this . close . emit ( ) ;
}
}
Estoy utilizando Input
para inyectar datos del home.component
al hacer clic llamando a una function
y estoy usando Output
para emit
una function
cuando el cuadro de diálogo está cerrado. El cuadro de diálogo se puede cerrar haciendo clic en la X en la parte superior derecha o haciendo clic en la superposición. La función closeModal()
eliminará la clase active
, restablecerá todos los valores y emitirá una función para restablecer isOpen
en el home.component
.
<!-- dialog.component.html -->
< div id =" overlay " [ngClass] =" {'active': isOpen} " (click) =" closeModal() " >
< div class =" modal " (click) =" $event.stopPropagation() " >
< div class =" modal-header " >
< span class =" header-title " *ngIf =" selectedMovie != null " >
{{selectedMovie?.title ? selectedMovie?.title : selectedMovie?.name}}
({{selectedMovie?.release_date ? (selectedMovie?.release_date | date: 'y') : selectedMovie?.first_air_date | date: 'y'}})
</ span >
< span class =" right " (click) =" closeModal() " > X </ span >
</ div >
< div class =" content " >
< div id =" top " class =" row " >
< div *ngIf =" trailerUrl != null " class =" col-trailer " >
< iframe [src] =" trailerUrl " width =" 560 " height =" 315 " frameborder =" 0 " allowfullscreen > </ iframe >
</ div >
< div class =" col-overview " >
< span class =" star-rating " *ngFor =" let star of selectedMovie?.rating " >
< span > ☆ </ span >
</ span >
< span >
{{selectedMovie?.rating?.length}}/10
</ span >
< br >
< hr >
< span > {{selectedMovie?.overview}} </ span > < br >
</ div >
</ div >
</ div >
</ div >
</ div >
trailerUrl
de YouTube y muestre en un <iframe>
. El selectedMovie
se pasa desde home.component
cuando se hace clic en el enlace de la película, pero antes de abrir el diálogo, necesito buscar el trailer de la película de la API. Agregué una nueva llamada API al archivo api.ts
//home.component.ts
//line 17
public isOpen: boolean = false ;
public selectedMovie: any ;
public trailerUrl: any ;
//line 37
public openTrailer ( movie ) {
this . selectedMovie = movie ;
this . dataService . trailer ( movie . id ) . subscribe ( res => {
if ( res [ 0 ] != null ) {
if ( res [ 0 ] . site === 'YouTube' ) {
this . trailerUrl = this . sanitizer . bypassSecurityTrustResourceUrl (
`https://www.youtube.com/embed/ ${ res [ 0 ] . key } `
) ;
this . isOpen = true ;
}
}
} )
}
Desde aquí, el data.service
funcionará como gerente intermedio para hablar con el server / api
y enviar la respuesta al cliente (front-end). El primer índice en la respuesta con Usally es un enlace a YouTube, donde residen casi todos los remolques de películas, así que estoy usando una condición para usar específicamente los remolques de YouTube y, si no, no abra el trailer. Por diversión, puede agregar a esta condición si desea dejar que el trailer se abra desde otra fuente de video.
//data.service.ts line 24
trailer ( item ) {
let searchterm = `query= ${ item } ` ;
try {
this . result = this . http . post ( '/trailer' , searchterm , { headers } ) ;
return this . result ;
} catch ( e ) {
console . log ( e , 'error' )
}
}
Estoy usando try catch
para manejar un error, pero hay muchas maneras de manejar un error en angular
. Esto fue solo por simplicidad de mi parte.
//server.ts line 42
server . post ( '/trailer' , async ( req , res ) => {
let searchquery = req . body . query ;
let encsearchquery = encodeURIComponent ( searchquery ) ;
const data = await api . data . trailer ( encsearchquery , apiKey ) ;
res . status ( 200 ) . json ( data ) ;
} )
Estoy utilizando una async function
mecanografiada que await
a la API para darnos la carga útil (respuesta) antes de completar la post
para evitar un error del servidor.
//api/api.ts
//line 4
let trailerInfo = [ ] ;
//line 19
methods . trailer = async ( id : string , apiKey : string ) => {
let apiUrl = `https://api.themoviedb.org/3/movie/ ${ id } /videos?api_key= ${ apiKey } &language=en-US` ;
let trailerPromise = new Promise ( ( resolve , reject ) => {
request ( apiUrl , { } , function ( err , res , body ) {
let data = JSON . parse ( body ) ;
trailerInfo = data [ 'results' ] ;
resolve ( ) ;
} ) ;
} ) ;
let result = await trailerPromise ;
return trailerInfo ;
} ;
Estoy usando este punto final TMDB https://api.themoviedb.org/3/movie/${id}/videos?api_key=${apiKey}&language=en-US
/videos?api_key=$ =pikeyh}&language=en-US para obtener el trailer de la película por id
de película. El id
y apikey
se pasan al punto final utilizando los soportes y los retroceso, que es una nueva forma de agregar valores dinámicos con JS y se ve mucho más agradable y luego usando A +
para concatenar los valores.
Si los datos cumplen con la condición de YouTube, se abre la ventana emergente del diablog y los datos se mostrarán dentro del HTML y las cadenas angular
interpoladas {{selectedMovie.title}}
, los dobles soportes procesan los datos en el HTML dinámicamente.
Algo de lo que no siempre se habla con proyectos como este es que no tomaría mucho tiempo convertir esto en un proyecto completamente diferente. Puede cambiar fácilmente los puntos finales en el archivo api.ts
para comunicarse con una API diferente y obtener datos diferentes para mostrar en la interfaz de usuario. Por supuesto, necesitaría cambiar algunas de las convenciones de nombres de variables para que el código tenga sentido, pero este proyecto se puede reciclar con algo más que podría estar más interesado. Véelo como una plantilla ya configurada con un servidor de backend simple y Archivo API para manejar cualquier datos que desee obtener y enviar de vuelta al front-end para su visualización. Cambie el título del encabezado en home.html
a algo como Job Search
y conéctese a una API de listado de trabajo que pueda obtener trabajos por palabras clave, por ejemplo. Una vez que comience, todo es posible. Gracias por codificar conmigo. Buena suerte. Código fuente completo
Nota al margen: Acabo de descubrir en este momento que hay una tag
de diálogo html5
<dialog open>This is an open dialog window</dialog>
pero no funcionó para mí en Chrome. Puede ser un poco nuevo y carente de soporte de navegador, pero tal vez usted los desarrolladores creativos pueden encontrar una manera de usarlo en lugar de mi enfoque de "hacerlo desde cero".
La API de TMDB recientemente agregó un nuevo punto final a su API que le dirá dónde se transmite una película utilizando la identificación de una película. También agregué otro punto final para obtener el elenco de las películas en la carga útil de la búsqueda. Actualicé el archivo api.ts
para manejar estas nuevas solicitudes.
Después de obtener los resultados de búsqueda, uso el método RXJS forkJoin
y map
para realizar múltiples solicitudes para providers
para cada película por id
. Los datos del proveedor se pushed
a la matriz mvProvider
y luego se agrega al objeto searchInfo
que luego se envía de vuelta al cliente con la matriz de resultados de la película, la matriz de proveedores y la matriz de créditos.
Dependiendo de la cantidad de resultados de búsqueda, una sola solicitud de búsqueda podría generar hasta 30 solicitudes de API antes de que los datos se resuelvan y se envíen al cliente. Creé una función sleep
para await
todas las solicitudes de finalizar antes de que una resolve
devuelva los datos de IMCOMPLETE al front-end. No es la mejor manera de manejar esto, pero funciona. El uso de async
y await
es importante asegurarse de que todos los datos que se obtengan se completen antes de que se resolved
la promise
.
//api.ts
let request = require ( 'request' )
let methods : any = { }
let searchInfo = [
{
results : [ ] ,
providers : [ ] ,
credits : [ ]
}
]
import { forkJoin } from 'rxjs'
methods . search = async ( term : string , apiKey : string ) => {
let type = 'movie'
let searchQuery = `https://api.themoviedb.org/3/search/ ${ type } ?api_key= ${ apiKey } &language=en-US&page=1&include_adult=false&query= ${ term } `
let searchPromise = new Promise ( ( resolve , reject ) => {
request ( searchQuery , { } , function ( err , res , body ) {
let mvProviders = [ ]
let mvCredits = [ ]
const sleep = ( ms ) => new Promise ( resolve => setTimeout ( resolve , ms ) )
let data = JSON . parse ( body )
searchInfo [ 0 ] [ 'results' ] = data [ 'results' ]
const providers = async ( ) => {
forkJoin (
data [ 'results' ] . map ( m =>
request (
`https://api.themoviedb.org/3/ ${ type } / ${ m . id } /watch/providers?api_key= ${ apiKey } ` ,
{ } ,
async function ( err , res , body ) {
let data = await JSON . parse ( body ) ;
mvProviders . push ( data ) ;
if ( mvProviders . length > 1 ) {
return searchInfo [ 0 ] [ 'providers' ] = mvProviders ;
}
}
)
)
)
}
const credits = async ( ) => {
await providers ( )
forkJoin (
data [ 'results' ] . map ( m =>
request (
`https://api.themoviedb.org/3/ ${ type } / ${ m . id } /credits?api_key= ${ apiKey } &language=en-US` ,
{ } ,
async function ( err , res , body ) {
let data = await JSON . parse ( body ) ;
mvCredits . push ( data ) ;
if ( mvCredits . length > 1 ) {
searchInfo [ 0 ] [ 'credits' ] = mvCredits ;
}
}
)
)
) ;
await sleep ( 1500 ) ;
resolve ( 'done' ) ;
}
credits ( )
} ) ;
} ) ;
let result = await searchPromise ;
return searchInfo ;
}
export const api = { data : methods } ;
Tuve que refactorizar el método de búsqueda de los tutoriales de Pevious porque me estaba bloqueando para agregar más datos de películas relacionados a la matriz antes de presentarlo en el archivo home.component.html
.
En el archivo html
puedo usar Angular
Event Binking (input)
para escuchar un cambio de evento y llamar a una función que pasa en el evento que contiene el texto de búsqueda.
Puedo pasar el texto de búsqueda al archivo api.ts
que lo agrega al api endpoint
y request
los datos.
Si la req
(solicitud) es exitosa, la carga útil RES (respuesta) se devuelve al cliente (front-end) y puedo formatear los datos con lógica dentro del home.component.ts
(controlador). Creé una array
llamada results
para definir los datos formateados para que los actores y el servicio de transmisión se muestren en la interfaz de usuario.
<!-- home.component.html -->
< div class =" row " >
< input (input) =" doSearch($event) " type =" search " class =" form-control " placeholder =" search " >
< span class =" search-title " > {{ results?.length ? 'Results' : 'Search' }} </ span >
</ div >
// home.component.ts
...
ngOnInit ( ) : void {
}
public doSearch ( e ) {
if ( e . target . value . length > 2 ) {
this . dataService . search ( e . target . value ) . pipe (
debounceTime ( 400 ) ) . subscribe ( res => {
this . format ( res )
} )
}
if ( e . target . value . length == 0 ) {
this . results = [ ]
this . selectedMovie = null
}
}
public format ( data ) {
this . util . relatedInfo ( data [ 0 ] . results , data [ 0 ] . providers , 'providers' , 'movies' )
this . util . relatedInfo ( data [ 0 ] . results , data [ 0 ] . credits , 'credits' , 'movies' )
this . loading = false
this . results = data [ 0 ] . results
console . log ( this . results , 'res' )
}
...
Vaya a Full home.component.ts
File.
Necesito formatear los datos para que coincidan con providers
y credits
con películas a las que pertenecen por identificación. Creé un service
de utilidad para manejar el formato. Al usar el comando ng gs services/util
puedo generar un archivo de servicio con Angular cli
. Las funciones en un service
pueden ser utilizadas por todos components
de una aplicación angular importando el service
al controlador component
. Ahora puedo llamar a la función relatedInfo
en home.component.ts
.
//util.service.ts
public relatedInfo ( items , extras , type : string , itemType : string ) {
for ( let item of items ) {
for ( let e of extras ) {
if ( item . id === e . id ) {
if ( type === 'credits' ) {
item . credits = e ;
item . type = itemType ;
if ( e . cast [ 0 ] != null ) {
item . credit1 = e . cast [ 0 ] [ 'name' ] ;
item . credit1Pic = e . cast [ 0 ] [ 'profile_path' ] ;
item . credit1Char = e . cast [ 0 ] [ 'character' ] ;
}
if ( e . cast [ 1 ] != null ) {
item . credit2 = e . cast [ 1 ] [ 'name' ] ;
item . credit2Pic = e . cast [ 1 ] [ 'profile_path' ] ;
item . credit2Char = e . cast [ 1 ] [ 'character' ] ;
}
}
if ( type === 'providers' ) {
item . provider = e [ 'results' ] . US != null ? e [ 'results' ] . US : 'unknown' ;
}
}
}
}
return items ;
}
Necesito mostrar los créditos en la vista de búsqueda y la vista del trailer. Dado que el HTML ya está configurado para la matriz de results
, hay algunos ajustes que necesito hacer en home.component.html
y dialog.component.html
para mostrar los créditos de actuación de la película.
<!-- home.component.html line 21 -->
...
< span class =" info " >
< p >
< span *ngIf =" item?.credit1 " > Cast: {{item?.credit1}}, {{item?.credit2}} < br > </ span >
< span class =" star-rating " *ngFor =" let star of rating(item) " > < span > ☆ </ span > </ span > < br >
< span class =" trailer-link " (click) =" openTrailer(item) " > Trailer </ span >
</ p >
</ span >
...
<!-- dialog.component.html line 24 -->
...
< span > {{selectedMovie?.overview}} </ span > < br >
< span class =" row cast " *ngIf =" selectedMovie?.credits " >
< span class =" col-cast " *ngFor =" let actor of selectedMovie?.credits?.cast; let i = index " >
< span [ngClass] =" {'dn': i > 3 || actor?.profile_path == null, 'cast-item': i <= 3 && actor?.profile_path != null} " >
< img src =" https://image.tmdb.org/t/p/w276_and_h350_face{{actor?.profile_path}} " alt =" image of {{actor?.name}} " > < br >
{{actor?.name}} < br > ({{actor?.character}})
</ span >
</ span >
</ span >
...
Agregué algunos nuevos estilos CSS para manejar estos nuevos datos.
/* dialog.component.scss line 111 */
...
.row.cast {
margin-top : 20 px ;
.col-cast {
flex : 0 0 50 % ;
text-align : left ;
font-size : 10 px ;
img {
width : 30 % ;
border : 1 px solid #fff ;
border-radius : 8 px ;
}
.cast-item {
padding-bottom : 5 px ;
display : block ;
}
.dn {
display : none ;
}
}
}
...
dialog.component.scss
completo.component.scss aquí.
Para mostrar el servicio de transmisión de cada película en la matriz de películas devueltas, uso una function
llamada getProvider
. Esta función tomará un argumento y para cada bucle a través de los datos del proveedor y definirá una string
corta para que se ajuste en la pantalla. Algunos títulos de proveedores son demasiado grandes para la interfaz de usuario y deben cambiarse usando if / else
conditonals.
<!-- home.component.html line 28 -->
< span *ngIf =" item?.provider != null " class =" {{getProvider(item?.provider)}} probar " >
{{getProvider(item?.provider)}}
</ span >
// home.component.ts line 75
...
public getProvider ( pro ) {
try {
if ( pro === 'unknown' ) {
return ''
} else if ( pro [ 'flatrate' ] != null ) {
if ( pro . flatrate [ 0 ] . provider_name === 'Amazon Prime Video' ) {
return 'prime'
} else if ( pro . flatrate [ 0 ] . provider_name . toUpperCase ( ) === 'IMDB TV AMAZON CHANNEL' ) {
return 'imdb tv'
} else if ( pro . flatrate [ 0 ] . provider_name . toUpperCase ( ) === 'APPLE TV PLUS' ) {
return 'apple tv'
} else {
return pro . flatrate [ 0 ] . provider_name . toLowerCase ( )
}
} else if ( pro [ 'buy' ] != null ) {
return ''
} else if ( pro [ 'rent' ] != null ) {
return ''
} else {
return ''
}
} catch ( e ) {
console . log ( e , 'error' )
}
}
...
Ahora, usando la class
Probar, puedo diseñar el proveedor de servicios de transmisión. También puedo usar el nombre del proveedor en la clase HTML utilizando soportes rizados de interpolación angular para agregar un color de fondo dinámico al estilo probar.
/* styles.scss line 58 */
...
.probar {
position : absolute ;
bottom : 0 ;
width : 100 % ;
text-align : center ;
font-size : 14 px ;
font-weight : 300 ;
letter-spacing : 2 px ;
opacity : 0.8 ;
color : #fff ;
text-transform : uppercase ;
background : #555 ;
}
.netflix {
background : #e00713 ;
}
.hbo {
background : #8440C1 ;
}
.prime {
background : #00A3DB ;
}
.hulu {
background : #1DE883 ;
}
.disney {
background : #111D4E ;
}
.apple {
background : #000 ;
}
...
Decido hacer modal del trailer el ancho completo de la pantalla y agregar una imagen de fondo de la película seleccionada. Actualicé dialog.component.html
utilizando la directiva angular [ngStyle]
para manejar la imagen de fondo de la película. Agregué una clase llamada full
a la clase modal
, luego agregué nuevos estilos a la clase modal.full
de Active.
<!-- dialog.component.html line 11 -->
...
< div id =" top " class =" row " *ngIf =" isOpen " [ngStyle] =" {'background': 'linear-gradient(-225deg, rgba(0,0,0,0.6) 50%, rgba(0,0,0,0.6) 80%), url(https://image.tmdb.org/t/p/w533_and_h300_bestv2/'+ selectedMovie?.backdrop_path +')' } " >
...
/* dialog.component.scss line 133 */
...
.modal.full {
height : 100 % ;
min-width : 100 % ;
#top {
height : 500 px ;
background-size : cover !important ;
background-repeat : no-repeat !important ;
}
.row .col {
flex : 0 0 11 % ;
}
.content {
.col-trailer {
align-self : center ;
text-align : center ;
}
.col-overview {
align-self : center ;
}
}
}
...
Esta es una característica genial que uso a menudo para encontrar dónde se transmite una película y la búsqueda responde rápidamente, más rápido que una búsqueda en Google, creo.
Un desafío divertido para los desarrolladores avanzados que leen esto sería si pudiera refactorizar el código api.ts
Estoy usando Promises
anidadas de esperar una resolve
antes de enviar los datos de la película al cliente. Sin embargo, no me gusta la forma en que tuve que usar una función de tiempo await
sleep
para que los datos esperen a que los datos credit
y provider
vuelvan a ser antes de que los datos de la película se enviaran al client
. No debería tener que usar la function
sleep
que era solo un parche para ayudarme. El problema era que creé demasiados ámbitos y las resoluciones no funcionaban de la manera que quería. Si alguien puede hacer que las resoluciones funcionen sin usar la function
del temporizador sleep
, estaría muy impresionado. ¡Gracias por leer!