Project using React/Cloning Netflix
React - Create Detail Container and Presenter
Cog Factory
2021. 5. 9. 11:12
Components/Poster.js
import styled from "styled-components";
import PropTypes from "prop-types";
import { Link } from "react-router-dom";
const Container = styled.div``;
const Image = styled.div`
background-image: url(${(props) => props.bgUrl});
width: 100%;
height: 180px;
background-repeat: no-repeat;
background-size: cover;
background-position: center;
transition: opacity 0.1s linear;
`;
const Rating = styled.span`
bottom: 5px;
right: 5px;
position: absolute;
opacity: 0;
transition: opacity 0.1s linear;
`;
const ImageContainer = styled.div`
position: relative;
&:hover {
${Image} {
opacity: 0.3;
}
${Rating} {
opacity: 1;
}
}
`;
const Title = styled.span`
display: block;
margin-bottom: 3px;
`;
const Year = styled.span`
font-size: 10px;
color: rgba(255, 255, 255, 0.5);
`;
const Poster = ({ id, imageUrl, title, rating, year, isMovie = false }) => (
<Link to={isMovie ? `/movie/${id}` : `/tv/${id}`}>
<Container>
<ImageContainer>
<Image
bgUrl={
imageUrl
? `https://image.tmdb.org/t/p/w300${imageUrl}`
: require("../assets/noPosterSmall.png")
}
/>
<Rating>
<span role="img" aria-label="rating">
⭐️
</span>{" "}
{rating}/10
</Rating>
</ImageContainer>
<Title>
{title.length > 18 ? `${title.substring(0, 18)}...` : title}
</Title>
<Year>{year}</Year>
</Container>
</Link>
);
Poster.propTypes = {
id: PropTypes.number.isRequired,
imageUrl: PropTypes.string,
title: PropTypes.string.isRequired,
rating: PropTypes.number,
year: PropTypes.string,
isMovie: PropTypes.bool,
};
export default Poster;
Poster에 Link를 추가했다. 이제 해당 링크로 Detail page를 갈 수 있다.
Detail/DetailContainer.js
import React from "react";
import DetailPresenter from "./DetailPresenter";
import { moviesApi, tvApi } from "api";
export default class extends React.Component {
constructor(props) {
super(props);
const {
location: { pathname },
} = props;
this.state = {
result: null,
loading: true,
error: null,
isMovie: pathname.includes("/movie/"),
};
}
async componentDidMount() {
const {
match: {
params: { id },
},
} = this.props;
let result = null;
const { isMovie } = this.state;
try {
if (isMovie) {
({ data: result } = await moviesApi.movieDetail(id));
} else {
({ data: result } = await tvApi.showDetail(id));
}
this.setState({
result,
});
} catch {
this.setState({
error: "Can't find anything.",
});
} finally {
this.setState({
loading: false,
});
}
}
render() {
const { result, error, loading } = this.state;
return <DetailPresenter result={result} error={error} loading={loading} />;
}
}
ComponentDidMount()를 사용하면 this.props를 사용할 수 있다. prop에는 <Route>가 넘겨주는 location과 params이 있다. isMovie는 url을 읽고 해당 Poster가 Movie Poster인지 TV Poster인지 확인한다.
Detail/DetailPresenter.js
import React from "react";
import PropTypes from "prop-types";
import styled from "styled-components";
import Helmet from "react-helmet";
import Loader from "Components/Loader";
const Container = styled.div`
height: calc(100vh - 50px);
width: 100%;
position: relative;
padding: 50px;
`;
const Backdrop = styled.div`
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url(${(props) => props.bgImage});
background-position: center center;
background-size: cover;
filter: blur(3px);
opacity: 0.5;
z-index: 0;
`;
const Content = styled.div`
display: flex;
width: 100%;
position: relative;
z-index: 1;
height: 100%;
`;
const Cover = styled.div`
width: 30%;
background-image: url(${(props) => props.bgImage});
background-position: center center;
background-size: cover;
height: 100%;
border-radius: 5px;
`;
const Data = styled.div`
width: 70%;
margin-left: 10px;
`;
const Title = styled.h3`
font-size: 32px;
`;
const ItemContainer = styled.div`
margin: 20px 0;
`;
const Item = styled.span``;
const Divider = styled.span`
margin: 0 10px;
`;
const Overview = styled.p`
font-size: 12px;
opacity: 0.7;
line-height: 1.5;
width: 50%;
`;
const DetailPresenter = ({ result, loading, error }) =>
loading ? (
<>
<Helmet>
<title>Loading | Nomflix</title>
</Helmet>
<Loader />
</>
) : (
<Container>
<Helmet>
<title>
{result.original_title ? result.original_title : result.original_name}{" "}
| Nomflix
</title>
</Helmet>
<Backdrop
bgImage={`https://image.tmdb.org/t/p/original${result.backdrop_path}`}
/>
<Content>
<Cover
bgImage={
result.poster_path
? `https://image.tmdb.org/t/p/original${result.poster_path}`
: require("../../assets/noPosterSmall.png")
}
/>
<Data>
<Title>
{result.original_title
? result.original_title
: result.original_name}
</Title>
<ItemContainer>
<Item>
{result.release_date
? result.release_date.substring(0, 4)
: result.first_air_date.substring(0, 4)}
</Item>
<Divider>•</Divider>
<Item>
{result.runtime ? result.runtime : result.episode_run_time[0]} min
</Item>
<Divider>•</Divider>
<Item>
{result.genres &&
result.genres.map((genre, index) =>
index === result.genres.length - 1
? genre.name
: `${genre.name} / `
)}
</Item>
</ItemContainer>
<Overview>{result.overview}</Overview>
</Data>
</Content>
</Container>
);
DetailPresenter.propTypes = {
result: PropTypes.object,
loading: PropTypes.bool.isRequired,
error: PropTypes.string,
};
export default DetailPresenter;
key
nowPlaying, upcoming, popular는 모두 배열이다. 이들을 map을 이용해서 <Poster>를 부르고 있다. 이 때 리스트 안에 있는 <Poster>에 key 값을 추가해야만 한다.
prop-types
설치
$ npm i prop-types
개요
prop-types는 prop의 type을 정의한다. prop을 받을 component 뒤에 method 형식으로 propTypes를 하고 객체를 정의한다.
위 코드와 같이 type을 정의하면 MoviePresenter가 받아들이는 prop이 옳지 않은 type일 때 error를 일으켜 개발자가 실수 하는 것을 막아준다. 만약 React를 typescript로 짰다면 prop-types는 필요가 없다.
참고 자료
- 노마드 코더의 React 멤버쉽 강의
- prop-types
- React propTypes 설명
소스 코드
github.com/zpskek/Nomflix-v2/commit/63c01c5343fe6abf64e1b0ab9590bd33d60f58d9