From 6430964f227f1df1ea5cb7263f421aea1c7e4169 Mon Sep 17 00:00:00 2001 From: Lucas Teixeira Date: Wed, 28 Oct 2020 23:12:08 -0300 Subject: [PATCH] WJ-24 - New: Person page; WJ-24 - New: Person request and parse; WJ-24 - New: Popular movies and filmography; --- src/components/Profile/dtos/ContainerProps.ts | 3 + src/components/Profile/dtos/index.ts | 6 + src/components/Profile/index.tsx | 27 +++++ src/components/Profile/styles.ts | 33 ++++++ src/components/index.ts | 1 + src/containers/Filmography/dtos/index.ts | 12 ++ src/containers/Filmography/index.tsx | 83 ++++++++++++++ src/containers/Filmography/styles.ts | 81 +++++++++++++ src/containers/index.ts | 1 + src/domains/Person/api/Details/Movie.ts | 11 ++ src/domains/Person/api/Details/Params.ts | 3 + src/domains/Person/api/Details/RawMovie.ts | 11 ++ src/domains/Person/api/Details/RawResponse.ts | 24 ++++ src/domains/Person/api/Details/Response.ts | 24 ++++ src/domains/Person/api/Details/index.ts | 86 ++++++++++++++ src/domains/Person/api/index.ts | 1 + src/pages/Movie/index.tsx | 2 +- src/pages/Person/dtos/Params.ts | 3 + src/pages/Person/index.tsx | 103 +++++++++++++++++ src/pages/Person/styles.ts | 106 ++++++++++++++++++ src/pages/index.ts | 1 + src/routes/index.tsx | 3 +- 22 files changed, 623 insertions(+), 2 deletions(-) create mode 100644 src/components/Profile/dtos/ContainerProps.ts create mode 100644 src/components/Profile/dtos/index.ts create mode 100644 src/components/Profile/index.tsx create mode 100644 src/components/Profile/styles.ts create mode 100644 src/containers/Filmography/dtos/index.ts create mode 100644 src/containers/Filmography/index.tsx create mode 100644 src/containers/Filmography/styles.ts create mode 100644 src/domains/Person/api/Details/Movie.ts create mode 100644 src/domains/Person/api/Details/Params.ts create mode 100644 src/domains/Person/api/Details/RawMovie.ts create mode 100644 src/domains/Person/api/Details/RawResponse.ts create mode 100644 src/domains/Person/api/Details/Response.ts create mode 100644 src/domains/Person/api/Details/index.ts create mode 100644 src/domains/Person/api/index.ts create mode 100644 src/pages/Person/dtos/Params.ts create mode 100644 src/pages/Person/index.tsx create mode 100644 src/pages/Person/styles.ts diff --git a/src/components/Profile/dtos/ContainerProps.ts b/src/components/Profile/dtos/ContainerProps.ts new file mode 100644 index 0000000..a543f27 --- /dev/null +++ b/src/components/Profile/dtos/ContainerProps.ts @@ -0,0 +1,3 @@ +export default interface ContainerProps { + large?: boolean; +} diff --git a/src/components/Profile/dtos/index.ts b/src/components/Profile/dtos/index.ts new file mode 100644 index 0000000..b1d158f --- /dev/null +++ b/src/components/Profile/dtos/index.ts @@ -0,0 +1,6 @@ +import ContainerProps from './ContainerProps'; + +export default interface PersonProps extends ContainerProps { + id: number; + profile?: string; +} diff --git a/src/components/Profile/index.tsx b/src/components/Profile/index.tsx new file mode 100644 index 0000000..2a53d68 --- /dev/null +++ b/src/components/Profile/index.tsx @@ -0,0 +1,27 @@ +import React, { useCallback } from 'react'; +import { useHistory } from 'react-router-dom'; + +import Route from 'routes/enums'; +import { Container, ProfileImage } from './styles'; + +import Props from './dtos'; + +const Movie: React.FC = ({ large = false, ...person }) => { + const history = useHistory(); + + const handleRedirect = useCallback(() => { + history.push(`${Route.PERSON}/${person.id}`); + }, [history, person.id]); + + if (!person.profile) { + return null; + } + + return ( + + + + ); +}; + +export default Movie; diff --git a/src/components/Profile/styles.ts b/src/components/Profile/styles.ts new file mode 100644 index 0000000..39a479e --- /dev/null +++ b/src/components/Profile/styles.ts @@ -0,0 +1,33 @@ +import styled from 'styled-components'; + +import { Color, PosterHeight, PosterWidth, Size } from 'shared/enums'; +import ContainerProps from './dtos/ContainerProps'; + +export const Container = styled.div` + position: relative; + width: ${props => (props.large ? PosterWidth.Large : PosterWidth.Default)}; + height: ${props => (props.large ? PosterHeight.Large : PosterHeight.Default)}; + border-radius: ${Size.Smallest}; + overflow: hidden; + + border: ${props => (props.large ? 0 : `1px solid ${Color.FillSecondary}`)}; + display: flex; + align-items: center; + justify-content: center; + + &:hover { + cursor: pointer; + + img { + width: 110%; + height: 110%; + } + } +`; + +export const ProfileImage = styled.img` + width: 100%; + height: 100%; + object-fit: cover; + transition: width 0.2s, height 0.2s; +`; diff --git a/src/components/index.ts b/src/components/index.ts index b5236b6..45f0743 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -6,4 +6,5 @@ export { default as Input } from './Input'; export { default as Movie } from './Movie'; export { default as MovieHighlight } from './MovieHighlight'; export { default as Person } from './Person'; +export { default as Profile } from './Profile'; export { default as Tooltip } from './Tooltip'; diff --git a/src/containers/Filmography/dtos/index.ts b/src/containers/Filmography/dtos/index.ts new file mode 100644 index 0000000..bfc1e5d --- /dev/null +++ b/src/containers/Filmography/dtos/index.ts @@ -0,0 +1,12 @@ +import DefaultProps from 'shared/dtos'; +import MovieProps from 'components/Movie/dtos'; +import FavoriteProps from 'domains/Favorites/api/List/Response'; +import { Color } from 'shared/enums'; + +export default interface Props extends DefaultProps { + title?: string; + data: any[]; + isLoading?: boolean; + loaderColor?: Color; + message?: string; +} diff --git a/src/containers/Filmography/index.tsx b/src/containers/Filmography/index.tsx new file mode 100644 index 0000000..5764ebf --- /dev/null +++ b/src/containers/Filmography/index.tsx @@ -0,0 +1,83 @@ +import React, { useCallback } from 'react'; +import { useHistory } from 'react-router-dom'; + +import Route from 'routes/enums'; + +import { ReactComponent as Loading } from 'assets/loading.svg'; +import { Wrapper } from 'components/Layout'; +import { + Container, + Title, + LoadingContainer, + ListContainer, + ItemContainer, + YearLabel, + MovieName, + CharacterName, + MessageContainer, +} from './styles'; + +import Props from './dtos'; + +const Filmography: React.FC = ({ + theme, + background, + color, + title, + data, + isLoading = false, + loaderColor, + message = 'Não há resultados.', +}) => { + const history = useHistory(); + + const handleRedirect = useCallback( + (movie: any) => { + if (movie.mediaType === 'movie') { + history.push(`${Route.MOVIE}/${movie.id}`); + } + }, + [history], + ); + + return ( + + + {title && ( + + {title} + + )} + + {isLoading && ( + + + + )} + + {!isLoading && data.length > 0 && ( + + {data.map(movie => ( + handleRedirect(movie)} + > + {movie.year} - + {movie.title || movie.name} + + {movie.character && ` como ${movie.character}`} + + + ))} + + )} + + {!isLoading && data.length === 0 && ( + {message} + )} + + + ); +}; + +export default Filmography; diff --git a/src/containers/Filmography/styles.ts b/src/containers/Filmography/styles.ts new file mode 100644 index 0000000..b689070 --- /dev/null +++ b/src/containers/Filmography/styles.ts @@ -0,0 +1,81 @@ +import styled from 'styled-components'; + +import DefaultProps from 'shared/dtos'; +import { Container as DefaultContainer } from 'components/Layout'; +import { Color, PosterHeight, Size } from 'shared/enums'; +import { getColor } from 'shared/utils'; + +interface LoadingProps { + loaderColor?: string; +} + +export const Container = styled(DefaultContainer)` + padding: ${Size.Medium} 0; + display: flex; + flex-direction: column; +`; + +export const Title = styled.h2` + margin-bottom: ${Size.Small}; + color: ${props => getColor(props.theme, props.color)}; + + @media (max-width: 1280px) { + margin-left: ${Size.Medium}; + } +`; + +export const LoadingContainer = styled.div` + display: flex; + align-items: center; + justify-content: center; + height: calc(${PosterHeight.Default}); + + svg { + height: ${Size.Largest}; + width: ${Size.Largest}; + fill: ${props => getColor(props.theme, props.loaderColor)}; + } +`; + +export const ListContainer = styled.div` + display: flex; + flex-direction: column; +`; + +export const ItemContainer = styled.div` + font-size: ${Size.Default}; + color: ${Color.Text}; + padding: ${Size.Medium} 0; + border-bottom: 1px solid ${Color.FillSecondary}; + text-decoration: none; + transition: background 0.4s; + + &:hover { + background: ${Color.FillSecondary}; + cursor: pointer; + } + + @media (max-width: 1280px) { + margin-left: ${Size.Medium}; + margin-right: ${Size.Medium}; + } +`; + +export const YearLabel = styled.span``; + +export const MovieName = styled.span` + font-weight: 500; +`; + +export const CharacterName = styled.span``; + +export const MessageContainer = styled.div` + display: flex; + align-items: center; + height: calc(${PosterHeight.Default} / 3); + color: ${Color.Text}; + + @media (max-width: 1280px) { + margin-left: ${Size.Medium}; + } +`; diff --git a/src/containers/index.ts b/src/containers/index.ts index d40e035..a6dfa7e 100644 --- a/src/containers/index.ts +++ b/src/containers/index.ts @@ -1,3 +1,4 @@ +export { default as Filmography } from './Filmography'; export { default as Footer } from './Footer'; export { default as Header } from './Header'; export { default as Highlights } from './Highlights'; diff --git a/src/domains/Person/api/Details/Movie.ts b/src/domains/Person/api/Details/Movie.ts new file mode 100644 index 0000000..070c716 --- /dev/null +++ b/src/domains/Person/api/Details/Movie.ts @@ -0,0 +1,11 @@ +import MovieDetails from 'domains/Movie/api/Details/Response'; + +export default interface Movie extends MovieDetails { + character: string; + year?: string; + originalDate: string; + + firstAirDate?: string; + name?: string; + mediaType?: string; +} diff --git a/src/domains/Person/api/Details/Params.ts b/src/domains/Person/api/Details/Params.ts new file mode 100644 index 0000000..e1d07a0 --- /dev/null +++ b/src/domains/Person/api/Details/Params.ts @@ -0,0 +1,3 @@ +export default interface Params { + appendToResponse?: string; +} diff --git a/src/domains/Person/api/Details/RawMovie.ts b/src/domains/Person/api/Details/RawMovie.ts new file mode 100644 index 0000000..85b71ab --- /dev/null +++ b/src/domains/Person/api/Details/RawMovie.ts @@ -0,0 +1,11 @@ +import RawMovieDetails from 'domains/Movie/api/Details/RawResponse'; + +export default interface RawMovie extends RawMovieDetails { + character: string; + year?: string; + originalDate: string; + + first_air_date?: string; + name?: string; + media_type?: string; +} diff --git a/src/domains/Person/api/Details/RawResponse.ts b/src/domains/Person/api/Details/RawResponse.ts new file mode 100644 index 0000000..2653590 --- /dev/null +++ b/src/domains/Person/api/Details/RawResponse.ts @@ -0,0 +1,24 @@ +// import Recommendations from 'domains/Movie/api/Recommendations/RawResponse'; +import RawMovie from 'domains/Person/api/Details/RawMovie'; + +export default interface RawResponse { + id: number; + name: string; + + biography: string; + popularity: number; + place_of_birth?: string; + profile_path?: string; + + known_for_department: string; + birthday?: string; + deathday?: string; + + gender: number; + also_known_as: string[]; + adult: boolean; + imdb_id: string; + homepage?: string; + + combined_credits?: { cast: RawMovie[] }; +} diff --git a/src/domains/Person/api/Details/Response.ts b/src/domains/Person/api/Details/Response.ts new file mode 100644 index 0000000..f1b15fa --- /dev/null +++ b/src/domains/Person/api/Details/Response.ts @@ -0,0 +1,24 @@ +// import Recommendations from 'domains/Movie/api/Recommendations/Response'; +import Movie from 'domains/Person/api/Details/Movie'; + +export default interface Response { + id: number; + name: string; + profile?: string; + + placeOfBirth?: string; + biography: string; + birthday?: string; + deathday?: string; + popularity: number; + + knownForDepartment: string; + gender: string; + alsoKnownAs: string[]; + adult: boolean; + imdbId: string; + homepage?: string; + + knownBy?: Movie[]; + filmography?: Movie[]; +} diff --git a/src/domains/Person/api/Details/index.ts b/src/domains/Person/api/Details/index.ts new file mode 100644 index 0000000..239a2fb --- /dev/null +++ b/src/domains/Person/api/Details/index.ts @@ -0,0 +1,86 @@ +import tmdb from 'services/api/tmdb'; + +import Params from 'domains/Person/api/Details/Params'; +import RawResponse from 'domains/Person/api/Details/RawResponse'; +import Response from 'domains/Person/api/Details/Response'; +import Movie from 'domains/Person/api/Details/Movie'; +// import Credits from 'domains/Person/api/Credits/Response'; +import { formatDate, formatTmdbImage } from 'shared/utils'; +// import Crew from 'domains/Person/api/Credits/dtos/Crew'; +// import Cast from 'domains/Person/api/Credits/dtos/Cast'; +// import { arrayToString } from 'shared/utils'; + +const Details = async ( + personId: number, + params?: Params, +): Promise => { + const response = await rawPopular(personId, params); + + return parseResponse(response); +}; + +export const rawPopular = async ( + personId: number, + params?: Params, +): Promise => { + const response = await tmdb.get(`/person/${personId}`, { + params: { append_to_response: params?.appendToResponse }, + }); + + return response.data; +}; + +const parseResponse = (person: RawResponse): Response => { + let parsedPerson = { + id: person.id, + name: person.name, + placeOfBirth: person.place_of_birth, + biography: person.biography, + popularity: person.popularity, + + knownForDepartment: person.known_for_department, + gender: person.gender === 2 ? 'Masculino' : 'Feminino', + alsoKnownAs: person.also_known_as, + adult: person.adult, + imdbId: person.imdb_id, + homepage: person.homepage, + + profile: formatTmdbImage({ value: person.profile_path }), + birthday: formatDate({ value: person.birthday }), + deathday: formatDate({ value: person.deathday }), + } as Response; + + const movies = person.combined_credits?.cast + .map(movie => ({ + poster: formatTmdbImage({ value: movie.poster_path }), + backdrop: formatTmdbImage({ value: movie.poster_path }), + id: movie.id, + title: movie.title, + favorite: false, + character: movie.character, + releaseDate: formatDate({ + value: movie.release_date || movie.first_air_date, + }), + originalDate: movie.release_date || movie.first_air_date, + year: + (movie.release_date && movie.release_date.substring(0, 4)) || + (movie.first_air_date && movie.first_air_date.substring(0, 4)), + popularity: movie.popularity, + name: movie.name, + mediaType: movie.media_type, + })) + .filter(item => item.originalDate) as Movie[]; + + const knownBy = [...movies].sort((a, b) => + a.popularity < b.popularity ? 1 : -1, + ); + const filmography = [...movies].sort((a, b) => + Date.parse(a.originalDate) < Date.parse(b.originalDate) ? 1 : -1, + ); + + parsedPerson = { ...parsedPerson, knownBy, filmography }; + + return parsedPerson; +}; + +export default Details; diff --git a/src/domains/Person/api/index.ts b/src/domains/Person/api/index.ts new file mode 100644 index 0000000..a9062ec --- /dev/null +++ b/src/domains/Person/api/index.ts @@ -0,0 +1 @@ +export { default as Details } from './Details'; diff --git a/src/pages/Movie/index.tsx b/src/pages/Movie/index.tsx index 45b34da..c915021 100644 --- a/src/pages/Movie/index.tsx +++ b/src/pages/Movie/index.tsx @@ -36,7 +36,7 @@ const Movie: React.FC = () => { try { setIsLoading(true); const params = { - appendToResponse: 'recommendations,credits', + appendToResponse: 'videos,images,recommendations,keywords,credits', }; const response = await Details(+id, params); diff --git a/src/pages/Person/dtos/Params.ts b/src/pages/Person/dtos/Params.ts new file mode 100644 index 0000000..09eef17 --- /dev/null +++ b/src/pages/Person/dtos/Params.ts @@ -0,0 +1,3 @@ +export default interface Params { + id: string; +} diff --git a/src/pages/Person/index.tsx b/src/pages/Person/index.tsx new file mode 100644 index 0000000..9adaf5f --- /dev/null +++ b/src/pages/Person/index.tsx @@ -0,0 +1,103 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { useParams } from 'react-router-dom'; + +import Params from 'pages/Person/dtos/Params'; +import PersonDetails from 'domains/Person/api/Details/Response'; +import { Color } from 'shared/enums'; +import { Details } from 'domains/Person/api'; + +import { ColumnLayout, Container, Profile } from 'components'; +import { Header, MovieList, Footer, Filmography } from 'containers'; +import { + ContentContainer, + PersonContainer, + ProfileContainer, + PersonDetailsContainer, + TitleContainer, + Title, + Subtitle, + OverviewContainer, + OverviewTitle, + Overview, + HeaderBackground, +} from './styles'; + +const Person: React.FC = () => { + const { id } = useParams(); + const [person, setMovie] = useState({} as PersonDetails); + const [isLoading, setIsLoading] = useState(false); + + const getPerson = useCallback(async () => { + try { + setIsLoading(true); + const params = { + appendToResponse: 'images,combined_credits,external_ids', + }; + + const response = await Details(+id, params); + + setMovie(response); + return response; + } catch (error) { + console.log('getPerson -> error', error); + } finally { + setIsLoading(false); + } + }, [id]); + + useEffect(() => { + window.scrollTo(0, 0); + getPerson(); + }, [getPerson]); + + return ( + +
+ + + + + + + + + + + {person.name} + + {person.birthday} | {person.gender} | {person.placeOfBirth} + + + {/* {person.tagline && {`"${person.tagline}"`}} */} + + Biografia + + {person.biography ? person.biography : 'Sem informações.'} + + + + + + + + + + +