Closed15
Netflixのクローンを作るチュートリアルをやってみる
眠れないので、眠くなるまでやる!
やってみる本
git
まずは、TMDBのAPIKEYを取得しpostmanで確認する
ちゃんと値が返ってきた。
Reactの環境構築
nextで構築済
API取得の下準備をする
いらないファイルを消す
ついでにNetlifyにデプロイしておく
axios.jsの作成とrequest.jsの作成
$ npm i axios
axios.ts
import axios from 'axios';
//TMDBからのbaseURLリクエストを作成
export const axiosInstance = axios.create({
baseURL: 'https://api.themoviedb.org/3',
});
.env.local
NEXT_PUBLIC_TMDB_API_KEY=XXXXXXXXXXXXX
request.ts
export const requests = {
fetchTrending: `/trending/all/week?api_key=${process.env.NEXT_PUBLIC_TMDB_API_KEY}&language=en-us`,
fetchNetflixOriginals: `/discover/tv?api_key=${process.env.NEXT_PUBLIC_TMDB_API_KEY}&with_networks=213`,
fetchTopRated: `/discover/tv?api_key=${process.env.NEXT_PUBLIC_TMDB_API_KEY}&languager=en-us`,
fetchActionMovies: `/discover/tv?api_key=${process.env.NEXT_PUBLIC_TMDB_API_KEY}&with_genres=28`,
fetchComedyMovies: `/discover/tv?api_key=${process.env.NEXT_PUBLIC_TMDB_API_KEY}&with_genres=35`,
fetchHorrorMovies: `/discover/tv?api_key=${process.env.NEXT_PUBLIC_TMDB_API_KEY}&with_genres=27`,
fetchRomanceMovies: `/discover/tv?api_key=${process.env.NEXT_PUBLIC_TMDB_API_KEY}&with_genres=10749`,
fetchDocumentMovies: `/discover/tv?api_key=${process.env.NEXT_PUBLIC_TMDB_API_KEY}&with_genres=99`,
} as const;
Chapter 04
Row.tsx
import { VFC, useState, useEffect } from 'react';
import { axiosInstance } from '@src/helper';
import { requests } from '@src/configs';
import { AspectRatio } from '@src/components/AspectRatio';
//画像のベースurl
const baseURL = 'https://image.tmdb.org/t/p/original' as const;
type RowPropsType = {
title: string;
fetchUrl: typeof requests[keyof typeof requests];
large?: boolean;
};
type Movie = {
id: string;
name: string;
title: string;
original_name: string;
poster_path: string;
backdrop_path: string;
};
export const Row: VFC<RowPropsType> = ({ title, fetchUrl, large = false }) => {
//TODO hooksに切り出し
//movieのdataを管理する配列
const [movies, setMovies] = useState<Movie[]>([]);
//movieのdataを取得する
useEffect(() => {
(async () => {
const request = await axiosInstance.get(fetchUrl);
setMovies(request.data.results);
})();
}, [fetchUrl]);
return (
<section className="text-white">
<h2 className="">{title}</h2>
<div className="mt-5">
<ul className="overflow-x-auto flex gap-x-5">
{movies.map((movie, index) => (
<li
className={`flex-shrink-0 ${large ? 'w-40' : 'w-44'}`}
key={`${movie.id}_${index}`}
>
<div className="relative overflow-hidden group cursor-pointer">
{large ? (
<AspectRatio width={1500} height={2250} />
) : (
<AspectRatio width={1920} height={1080} />
)}
<img
className="absolute inset-0 object-cover object-center transition-transform duration-300 ease-out transform group-hover:scale-110"
src={`${baseURL}${
large ? movie.poster_path : movie.backdrop_path
}`}
alt={movie.name}
decoding="async"
/>
</div>
</li>
))}
</ul>
</div>
</section>
);
};
index.tsx
import { VFC } from 'react';
import { Layout, Head } from '@src/layouts';
import { Row } from '@src/components';
import { requests } from '@src/configs';
import { PageContext } from '@src/store';
import { usePageReducer } from '@src/hooks';
const Page: VFC = () => {
const currentPage = usePageReducer('1');
return (
<PageContext.Provider value={currentPage}>
<Layout>
<Head />
<Row
title="NETFLIX ORIGUINALS"
fetchUrl={requests.fetchNetflixOriginals}
large={true}
/>
<Row title="Top Rated" fetchUrl={requests.fetchTopRated} />
<Row title="Action Movies" fetchUrl={requests.fetchActionMovies} />
<Row title="Comedy Movies" fetchUrl={requests.fetchComedyMovies} />
<Row title="Horror Movies" fetchUrl={requests.fetchHorrorMovies} />
<Row title="Romance Movies" fetchUrl={requests.fetchRomanceMovies} />
<Row title="DOcumentaries" fetchUrl={requests.fetchDocumentMovies} />
</Layout>
</PageContext.Provider>
);
};
export default Page;
Chapter 05
MovieとbaseURLをリファクタリング
$ npm i @tailwindcss/line-clamp
Hero.tsx
import {VFC, useState, useEffect} from "react";
import {Movie} from "@src/components/type";
import {baseURL, requests} from "@src/configs";
import {axiosInstance} from "@src/helper";
type HeroPropsType = {}
export const Hero: VFC<HeroPropsType> = () => {
//とりあえず初期値を入力しておく
//TODO リファクタリング
const [movie, setMovie] = useState<Movie>({
id: '',
name: '',
title: '',
original_name: '',
poster_path: '',
backdrop_path: '',
overview: ''
});
//movieのdataを取得する
//TODO hooksに切り出す
useEffect(() => {
(async () => {
const request = await axiosInstance.get(requests.fetchNetflixOriginals);
//TODO asをなくす
const results = request.data.results as Movie[];
//配列からランダムな値を一つ返す
//TODO 関数切り出し
const randomData = results[Math.floor(Math.random() * results.length - 1)]
setMovie(randomData);
})();
}, []);
return (
<div className="h-96 relative overflow-hidden text-white">
<img className="absolute inset-0 h-full object-cover object-center z-10" aria-hidden={true} src={`${baseURL}${movie.backdrop_path}`} alt="" decoding="async"/>
<div className="w-full h-full flex items-center relative z-20 backdrop-filter backdrop-blur-sm bg-black bg-opacity-30">
<div className="wrapper">
<h1 className="text-32 font-bold leading-1.2 line-clamp-2">
{movie.original_name}
</h1>
<ul className="mt-4 flex gap-x-4">
<li>
<button className="text-12 text-white cursor-pointer font-bold rounded px-3 py-2 bg-gray-800 bg-opacity-80 transition-colors hover:text-black hover:bg-gray-200 focus:text-black focus:bg-gray-200" style={{minWidth: '100px'}} type="button">Play</button>
</li>
<li>
<button className="text-12 text-white cursor-pointer font-bold rounded px-3 py-2 bg-gray-800 bg-opacity-80 transition-colors hover:text-black hover:bg-gray-200 focus:text-black focus:bg-gray-200" style={{minWidth: '100px'}} type="button">My List</button>
</li>
</ul>
<div className="mt-4 w-full line-clamp-3 sm:w-90">
<p className="text-12 font-bold leading-1.5">{movie.overview}</p>
</div>
</div>
</div>
<div className="absolute inset-x-0 bottom-0 z-30 h-1/3 bg-gradient-to-b from-transparent to-black"/>
</div>
)
}
Chapter 06
$ npm i lodash @types/lodash
Header.tsx
import {VFC, useState, useEffect} from 'react';
import {AspectRatio} from "@src/components";
import {throttle} from 'lodash';
type HeaderPropsType = {};
export const Header: VFC<HeaderPropsType> = () => {
//交差しているかどうかのステート
const [intersection, setIntersection] = useState(false);
useEffect(() => {
//scroll event
//throttleで300ms間引く
const handleScrollEvent = throttle(() => {
//100px以上かどうか
const isIntersection = window.scrollY > 100;
setIntersection(isIntersection);
}, 300);
window.addEventListener("scroll", handleScrollEvent, false);
return () => {
window.removeEventListener("scroll", handleScrollEvent, false);
};
}, []);
return (
<header className={`fixed top-0 inset-x-0 z-50 backdrop-filter backdrop-blur-sm transition-colors duration-500 bg-black shadow-md ${intersection ? 'bg-opacity-100' : 'bg-opacity-0'}`}>
<div className="px-5 flex justify-between items-center h-17 sm:px-10">
<div className="relative w-20">
<AspectRatio width={625} height={169}/>
<img className="absolute left-0 top-0" src="/netflix.svg" alt="" width={625} height={169} decoding="async"/>
</div>
<div className="relative w-8">
<AspectRatio width={32} height={32}/>
<img className="absolute left-0 top-0" src="/avatar.png" alt="" width={32} height={32} decoding="async"/>
</div>
</div>
</header>
);
};
Chapter 07
$ npm i movie-trailer react-youtube
Row.tsx
import {VFC, useState, useEffect} from 'react';
import {axiosInstance} from '@src/helper';
import {requests, baseURL} from '@src/configs';
import {AspectRatio} from '@src/components/AspectRatio';
import {Movie} from '@src/components/type';
import YouTube from "react-youtube";
type RowPropsType = {
title: string;
fetchUrl: typeof requests[keyof typeof requests];
large?: boolean;
};
//trailerのoption
type Options = {
height: string;
width: string;
playerVars: {
autoplay: 0 | 1 | undefined;
};
};
const opts: Options = {
height: "390",
width: "640",
playerVars: {
// https://developers.google.com/youtube/player_parameters
autoplay: 1,
},
};
export const Row: VFC<RowPropsType> = ({title, fetchUrl, large = false}) => {
//TODO hooksに切り出し
//movieのdataを管理する配列
const [movies, setMovies] = useState<Movie[]>([]);
const [trailerUrl, setTrailerUrl] = useState<string | null>(null);
//movieのdataを取得する
useEffect(() => {
(async () => {
const request = await axiosInstance.get(fetchUrl);
setMovies(request.data.results);
})();
}, [fetchUrl]);
//画像をclickする時の関数
const handleClick = async (movie: Movie) => {
//trailerUrlをnullにする
setTrailerUrl(null);
try {
//動画を取得する
const response = await axiosInstance.get(`/movie/${movie.id}/videos?api_key=${process.env.NEXT_PUBLIC_TMDB_API_KEY}`);
//動画の配列の0番目のkeyにYouTubeのidが入っているので代入する
setTrailerUrl(response.data.results[0]?.key ?? null);
} catch (error) {
console.error(error);
}
}
return (
<section className="text-white">
<h2 className="">{title}</h2>
<div className="mt-5">
<ul className="overflow-x-auto flex gap-x-5">
{movies.map((movie, index) => (
<li
className={`flex-shrink-0 ${large ? 'w-40' : 'w-44'}`}
key={`${movie.id}_${index}`}
>
<button className="relative w-full overflow-hidden group cursor-pointer" onClick={() => handleClick(movie)} type="button">
{large ? (
<AspectRatio width={1500} height={2250}/>
) : (
<AspectRatio width={1920} height={1080}/>
)}
<img
className="absolute inset-0 object-cover object-center transition-transform duration-300 ease-out transform group-hover:scale-110"
src={`${baseURL}${
large ? movie.poster_path : movie.backdrop_path
}`}
alt={movie.name}
decoding="async"
/>
</button>
</li>
))}
</ul>
{trailerUrl && <YouTube videoId={trailerUrl} opts={opts}/>}
</div>
</section>
);
}
;
なぜかサムネイルとYouTubeのリンクが違う😂
ちょっと全体的にUIを整えたらmainにマージさせるか
色々見た目をきれいにした
実はNetflixを使ったことがないので、UIを知らない😇
リファクタリング
まずは、動画を取得している部分をhooksに切り出す
useMovies.ts
import {useEffect, useState, Dispatch, SetStateAction} from "react";
import {Movie} from "@src/components/type";
import {axiosInstance} from "@src/helper";
import {requests} from '@src/configs';
type useMoviesType = (
fetchUrl: typeof requests[keyof typeof requests]
) => {
movies: Movie[],
movie: Movie,
setMovies: Dispatch<SetStateAction<Movie[]>>
}
export const useMovies: useMoviesType = (fetchUrl) => {
//動画の情報が入った配列
const [movies, setMovies] = useState<Movie[]>([]);
//動画の情報が入った配列からランダムに一つ返した値
const [movie, setMovie] = useState<Movie>({
id: '',
name: '',
title: '',
original_name: '',
poster_path: '',
backdrop_path: '',
overview: '',
});
//moviesのdataを取得する
useEffect(() => {
(async () => {
const request = await axiosInstance.get(fetchUrl);
//TODO asをなくす
const results = request.data.results as Movie[];
setMovies(results);
//ランダムの部分は切り出し
setMovie(results[Math.floor(Math.random() * results.length - 1)])
})();
}, [fetchUrl]);
return {
movies,
movie,
setMovies
}
}
Row.tsx
-import { VFC, useState, useEffect } from 'react';
+import { VFC, useState } from 'react';
import { axiosInstance } from '@src/helper';
import { requests, baseURL } from '@src/configs';
import { AspectRatio } from '@src/components/AspectRatio';
import { Movie } from '@src/components/type';
import YouTube from 'react-youtube';
+import {useMovies} from "@src/hooks/useMovies";
// 間は省略
export const Row: VFC<RowPropsType> = ({ title, fetchUrl, large = false }) => {
- //TODO hooksに切り出し
//movieのdataを管理する配列
- const [movies, setMovies] = useState<Movie[]>([]);
+ const {movies} = useMovies(fetchUrl);
const [trailerUrl, setTrailerUrl] = useState<string | null>(null);
- //movieのdataを取得する
- useEffect(() => {
- (async () => {
- const request = await axiosInstance.get(fetchUrl);
- setMovies(request.data.results);
- })();
- }, [fetchUrl]);
-
//画像をclickする時の関数
const handleClick = async (movie: Movie) => {
//trailerUrlをnullにする
Hero.tsx
-import { VFC, useState, useEffect } from 'react';
-import { Movie } from '@src/components/type';
+import { VFC } from 'react';
import { baseURL, requests } from '@src/configs';
-import { axiosInstance } from '@src/helper';
+import {useMovies} from "@src/hooks/useMovies";
type HeroPropsType = {};
export const Hero: VFC<HeroPropsType> = () => {
- //とりあえず初期値を入力しておく
- //TODO リファクタリング
- const [movie, setMovie] = useState<Movie>({
- id: '',
- name: '',
- title: '',
- original_name: '',
- poster_path: '',
- backdrop_path: '',
- overview: '',
- });
-
- //movieのdataを取得する
- //TODO hooksに切り出す
- useEffect(() => {
- (async () => {
- const request = await axiosInstance.get(requests.fetchNetflixOriginals);
-
- //TODO asをなくす
- const results = request.data.results as Movie[];
- //配列からランダムな値を一つ返す
- //TODO 関数切り出し
- const randomData =
- results[Math.floor(Math.random() * results.length - 1)];
- setMovie(randomData);
- })();
- }, []);
+ const {movie} = useMovies(requests.fetchNetflixOriginals);
return (
<div className="h-105 relative overflow-hidden text-white">
だいぶスッキリした!
リファクタリング
axios.tsをuseMoviesに移動し削除
axiosのresponseの型を定義(ちゃんとした型ではない)
useMovies.ts
//moviesのdataを取得する
useEffect(() => {
(async () => {
- const request = await axiosInstance.get(fetchUrl);
- //TODO asをなくす
- const results = request.data.results as Movie[];
+ const request = await axiosInstance.get<{results: Movie[]}>(fetchUrl);
+ const results = request.data.results;
setMovies(results);
//ランダムの部分は切り出し
setMovie(results[Math.floor(Math.random() * results.length - 1)])
トレーラーの動画取得の部分でも使ってた😂
まぁここは別の動画が出てくるのでAPI使ってタイトルからトレーラーを探す感じに変えていく
リファクタリング編一旦終わり
このスクラップは2021/08/25にクローズされました