環境の準備
①ターミナルでreactアプリケーションを作成する。
$ npx create-react-app <プロジェクト名>
$ cd <プロジェクト名>
$ npm start
② 使用しないファイルを削除する。
App.test.css
logo.svg
reportWebVitals.js
setupTest.js
③ 必要なパッケージをインストールする。
公式サイト:Tailwind CSS
$ 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