Closed15

Netflixのクローンを作るチュートリアルをやってみる

HishoHisho

まずは、TMDBのAPIKEYを取得しpostmanで確認する

HishoHisho

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;

https://github.com/hisho/Netflix-clone/tree/feature/chapter_03

HishoHisho

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;


https://github.com/hisho/Netflix-clone/tree/feature/chapter_04

HishoHisho

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>
  )
}


https://github.com/hisho/Netflix-clone/tree/feature/chapter_05

HishoHisho

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>
  );
};

https://github.com/hisho/Netflix-clone/tree/feature/chapter_06

HishoHisho

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>
    );
  }
;

https://github.com/hisho/Netflix-clone/tree/feature/chapter_07

なぜかサムネイルとYouTubeのリンクが違う😂

HishoHisho

ちょっと全体的にUIを整えたらmainにマージさせるか

HishoHisho

リファクタリング

まずは、動画を取得している部分を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">

だいぶスッキリした!

HishoHisho

リファクタリング

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)])

このスクラップは2021/08/25にクローズされました