React（TailwindCSS）の構成でアプリを作成しました【Movie App】

## 環境の準備
①ターミナルでreactアプリケーションを作成する。

```
$ npx create-react-app <プロジェクト名>
$ cd <プロジェクト名>
$ npm start
```
② 使用しないファイルを削除する。

```
App.test.css
logo.svg  
reportWebVitals.js  
setupTest.js
```
③ 必要なパッケージをインストールする。

公式サイト:[Tailwind CSS](https://tailwindcss.com/)

```
$ npm install -D tailwindcss postcss autoprefixer
$ npm i tailwind-scrollbar-hide
$ npx tailwindcss init -p
```
```
$ yarn add axios react-router-dom
$ yarn add tailwindcss-scrollbar-hide
```

## コンポーネント・ファイル構成

```
 src
　 ├─ assets
       ├── loading_spinner.gif
       └── NavBar.jsx
  ├─ components
       ├── MovieCard.jsx
       └── NavBar.jsx
  ├─ pages
       ├── HomePage.jsx
       ├── MoviePage.jsx
       └── PageNotFound.jsx
  ├── App.jsx
  ├── index.css
  └── index.jsx
 ├── .env
 ├── craco.config.js
 ├── tailwind.config.js
```

```src/components/MovieCard.jsx
import { Link } from 'react-router-dom';

const MovieCard = ({ movie }) => {
  return (
    <Link to={`/movie/${movie.id}`}>
      <div
        className='w-[21rem] max-w-[100%] bg-black rounded-xl p-3 text-white m-5
      flex flex-col cursol-poinster text-xl hover:scale-110'
      >
        <img
          className='w-full self-center rounded-lg h-[476px]'
          src={'https://image.tmdb.org/t/p/original/' + movie.poster_path}
          alt='poster'
        />
        <h3 className='my-1'>{movie.title}</h3>
        <h3 className='my-1'>★{movie.vote_average}</h3>
      </div>
    </Link>
  );
};

export default MovieCard;
```
```src/components/Nabvar.jsx
import { Link } from 'react-router-dom';

const NavBar = () => {
  return (
    <div className='w-full'>
      <Link to='/'>
        <div className='text-white text-2xl font-bold md:text-4xl lg:text-5xl ml-4 pt-2'>
          Front-end Moivies App
        </div>
      </Link>
    </div>
  );
};

export default NavBar;
```
```src/pages/HomePage.jsx
import NavBar from '../components/NavBar';
import loading_spinner from './../assets/loading_spinner.gif';
import { useState, useEffect } from 'react';
import axios from 'axios';
import MovieCard from '../components/MovieCard';

async function getMovies(pageNo) {
  const res = await axios.get(
    `https://api.themoviedb.org/3/trending/movie/day?api_key=${process.env.REACT_APP_API_KEY}&page=${pageNo}`
  );
  console.log(res.data.results);
  return res.data.results;
}

const HomePage = () => {
  const [movies, setMovies] = useState('Loading');
  const [pageNo, setPageNo] = useState(1);

  useEffect(() => {
    getMovies(pageNo)
      .then((res) => {
        setMovies(res);
      })
      .catch((err) => {
        alert(err);
      });
  }, [pageNo]);
  if (movies === 'Loading' || !movies || movies.length === 0)
    return (
      <div className='flex items-center justify-center h-screen bg-gray-200'>
        <img src={loading_spinner} alt='loading' height='200px' width='200px' />
      </div>
    );
  else
    return (
      <div className='bg-gray-200 min-h-screen flex flex-col items-center h-full'>
        <NavBar />
        <div className='flex flex-warp justify-evenly'>
          {movies.map((movie) => (
            <MovieCard movie={movie} />
          ))}
        </div>
        <div className='w-[250] mt-5 pb-10 font-bold'>
          <button
            className='bg-white rounded-full px-4 mr-2 hover:border-black border-2 hover:font-bold'
            onClick={() => {
              if (pageNo > 1) setMovies('Loading');
              setPageNo(pageNo - 1);
            }}
          >
            Previous
          </button>
          {pageNo}
          <button
            className='bg-white rounded-full px-4 ml-2 hover:border-black border-2 hover:font-bold'
            onClick={() => {
              if (pageNo > 1000) setMovies('Loading');
              setPageNo(pageNo + 1);
            }}
          >
            Next
          </button>
        </div>
      </div>
    );
};

export default HomePage;
```
```src/pages/MoviePage.jsx
import { useNavigate, useParams } from 'react-router';
import { useState, useEffect } from 'react';
import loading_spinner from './../assets/loading_spinner.gif';
import axios from 'axios';
import NavBar from './../components/NavBar';
import play_icon from './../assets/play_icon.png';
async function getMovie(movieId) {
  const res =
    await axios.get(`https://api.themoviedb.org/3/movie/${movieId}?api_key=${process.env.REACT_APP_API_KEY}
    `);
  return res.data;
}

async function getClips(movieId) {
  const res =
    await axios.get(`https://api.themoviedb.org/3/movie/${movieId}/videos?api_key=${process.env.REACT_APP_API_KEY}

    `);
  return res.data.results;
}

function MoviePage() {
  const { movieId } = useParams();
  const [movie, setMovie] = useState('loading');
  const navigate = useNavigate();
  const [width, setWidth] = useState(window.screen.availWidth);
  const [clips, setClips] = useState([]);
  let mt = width > 786 ? (width * 9) / 16 - 250 : 0;

  window.addEventListener('resize', () => {
    setWidth(window.screen.availWidth);
  });

  useEffect(() => {
    getMovie(movieId)
      .then((res) => {
        setMovie(res);
        getClips(movieId)
          .then((res) => {
            setClips(res);
          })
          .catch((err) => {
            alert(err);
            navigate('/', { replace: true });
          });

        if (width > 786) {
          window.scroll({ top: mt - 100, behavior: 'smooth' });
        }
      })
      .catch((err) => {
        alert(err);
        navigate('/', { replace: true });
      });
  }, [movieId, navigate, mt, width]);

  if (movie === 'loading' || !movie) {
    return (
      <div className='bg-gray-300 h-screen flex items-center justify-center'>
        <img src={loading_spinner} alt='loading' />
      </div>
    );
  }

  return (
    <div className='bg-gray-300 min-h-[100vh] text-white text-3xl sm:text-4xl md:text-3xl lg:text-4xl xl:text-6xl font-bold'>
      {width < 768 ? (
        <NavBar />
      ) : (
        <img
          src={'https://image.tmdb.org/t/p/original/' + movie.backdrop_path}
          alt='backdrop'
          className='w-screen aspect-video absolute top-0'
        />
      )}
      <div
        className='flex flex-col items-center justify-center md:flex-row md:ml-[50px]'
        style={{
          marginTop: `${mt}px`,
        }}
      >
        <img
          src={'https://image.tmdb.org/t/p/original/' + movie.poster_path}
          alt='poster'
          className='rounded-xl border-white border-4 max-w-[min(400px,90%)] sm:max-w-[50%]  md:h-[576px] z-10'
        />
        <h1 className='z-10 md:ml-10 text-center'>{movie.title}</h1>
      </div>

      {/*Clips And Trailers Part */}
      <div className='mt-5 md:mt-10 text-xl md:text-2xl lg:text-4xl pb-[100px] mx-2 sm:mx-5 md:mx-[50px] lg:mx-[100px]'>
        <div className='mt-5 md:mt-10 text-lg md:text-xl lg:text-2xl'>
          <div>
            Release Date :-
            <span className=' font-normal'>{movie.release_date}</span>
          </div>
          <div>
            Duration :-
            <span className=' font-normal'>
              {parseInt(movie.runtime / 60)}:{movie.runtime % 60} hr
            </span>
          </div>
          <div>
            Rating :-
            <span className=' font-normal'>{movie.vote_average}/10</span>
          </div>
        </div>
        Clips And Trailers
        <div className='flex overflow-scroll scrollbar-hide snap-x mt-5 md:mt-10'>
          {clips.map((clip) => (
            <div
              className='ml-5'
              onClick={() => {
                window.open(`https://youtube.com/watch?v=${clip.key}`);
              }}
            >
              <div className='relative flex-shrink-0 h-[180px] md:h-[250px] lg:h-[300px] aspect-video rounded-xl'>
                <img
                  src={`https://img.youtube.com/vi/${clip.key}/hqdefault.jpg`}
                  className='absolute object-cover h-[180px] md:h-[250px] lg:h-[300px] aspect-video rounded-xl '
                  alt='youtube thumbnail'
                />
                <img
                  src={play_icon}
                  alt='play icon'
                  className='absolute inset-0 w-[150px] h-[150px] m-auto'
                />
              </div>
              <p className='text-lg md:text-xl font-normal mt-1'>{clip.name}</p>
            </div>
          ))}
        </div>
        {/* Overview */}
        <div className='mt-5 md:mt-10'>OverView</div>
        <div className='mt-5 md:mt-10 font-normal text-lg md:text-xl lg:text-2xl'>
          {movie.overview}
        </div>
      </div>
    </div>
  );
}

export default MoviePage;
```
```src/pages/PageNotFound.jsx
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router';

function PageNotFound() {
  const [time, setTime] = useState(5);
  const navigate = useNavigate();
  useEffect(() => {
    setTimeout(() => {
      setTime(time - 1);
      if (time === 0) {
        navigate('/', { replace: true });
      }
    }, 1000);
  }, [time, navigate]);
  return (
    <div className='bg-gray-700 h-screen flex flex-col items-center justify-center text-8xl font-bold'>
      <div className='text-red-600'>404</div>
      <div className='text-4xl text-white'>Page Not Found</div>
      <div className='text-4xl text-white'>
        Redirecting to home page in {time} sec
      </div>
    </div>
  );
}

export default PageNotFound;
```
```App.jsx
import { HashRouter, Routes, Route } from 'react-router-dom';
import HomePage from './pages/HomePage';
import MoviePage from './pages/MoviePage';
import PageNotFound from './pages/PageNotFound';

function App() {
  return (
    <HashRouter>
      <Routes>
        <Route exact path='/' element={<HomePage />} />
        <Route exact path='/movie/:movieId' element={<MoviePage />} />
        <Route exact path='*' element={<PageNotFound />} />
      </Routes>
    </HashRouter>
  );
}

export default App;
```
```index.css
@tailwind base;
@tailwind components;
@tailwind utilities;
```
```index.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
```
```.env
REACT_APP_API_KEY=" your key "
```
```tailwind.config.js
module.exports = {
  content: ['./src/**/*.{jsx,js}'],
  theme: {
    extend: {},
  },
  plugins: [require('tailwind-scrollbar-hide')],
};
```

## 参考サイト
[How To Use Tailwind JIT in React.JS | Tailwind JIT | React | May We Code](https://www.youtube.com/watch?v=1wxtBpeLKzg)