Github
Live Demo
簡介
這個為了練習使用 React 的邏輯以及編寫方式所刻的頁面。這裡練習了如何使用 TMDB 提供的 API 來獲取需要的資料、使用 useState 以及 useEffect來更新資料、拆分 component、使用 styled component 的方法。下面記錄了搜尋頁面 fetch API 的詳細步驟,其他頁面則只是記錄重點功能。參考資料附在文末。
另外,因為專案一直在添加新的功能,因此檔案名字以及 Code 都會有調整,並不完全跟筆記的一樣。
功能
- 首頁有即將上映之電影 & 熱門電影/電視劇 (可通過點擊按鈕切換)
- 電影的呈現方式使用橫軸的滾動條
- 電影頁則是以卡片的形式呈現,點擊卡片會出現電影資訊
- 彈出的 Modal 內含有電影預告的網址,導致外部鏈接
- 搜尋到的電影可以加入 favourite list 裡,同時也會加到 local storage 裡
- 已經儲存的電影可點擊菜單列的愛心查看
- 無法顯示的照片使用默認照片顯示
前置作業
- 需要使用的 API : The Movie Database API
- 先到 TMDB 網站申請賬號,再根據 官方文件 申請網站的 API key .文件上都有清楚的申請步驟,申請成功後,把 key 以及 Access Token 保存下來。
- Modal 以及 Pagination 使用 meterial ui
1
| $ npm install @material-ui/lab
|
步驟
搜尋頁面
設置基本結構以及樣式
簡單設置需要使用到的 class component ,在內添加 JRX 以及引入外部 CSS file 用以測試文件是否能夠渲染到瀏覽器上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| mport React from 'react'; import ReactDOM from 'react-dom'; import './index.css';
class Main extends React.Component{ render() { return ( <div className="container"> <header> <h1 className="title"> Search a movie.</h1> </header> </div> ) } }
ReactDOM.render(<Main />,document.getElementById('root'));
|
創造一個 component
使用 form 創造一個 searchMovie component ,裡面需要有 input, label 以及 submit button. 在文件的結尾 export 後,再於 index.js file 中引入 App.js.
1 2 3 4 5 6 7 8 9 10 11 12 13
| function Search(){ return ( <form className="form"> <label htmlFor="query">Movie name</label> <input type = "text" name = "query" placeholder = "Goziila vs Kong..." /> <button className="submitBtn" type="submit">submit</button> </form> ) }
|
透過 API 抓取需要的資料
首先創造 searchMovies function,在 button 添加 onSubmit attribute 指向剛才創造的function. 這個 function 要先將原本 onSubmit 預設的動作清除 (preventDefault) 加上我們想要的操作。在加上需要執行的代碼前,可以先簡單測試是否成功將預設的動作清除。
1 2 3 4 5 6 7
| const searchMovies = (e) =>{ e.preventDefault() console.log("submitting") }
>> submitting
|
API的組成如下,當中的搜索方式,TMDB 提供了了三種:search / discover /find,各有不同的功能,詳細的可以查看官方文件。
除了成功取得資料外,亦要考慮無法獲得資料的情況。因此可以使用 try
catch
來做 error handling.在還沒獲取用戶輸入的資料前,可以先設置query來測試 API 是否運作順利。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| const API_KEY = process.env.REACT_APP_API_KEY const searchMovies = async (e) =>{ e.preventDefault() console.log("submitting")
const query = "Jurassic Park"
const url = `https://api.themoviedb.org/3/search/movie/?api_key=${API_KEY}&language=en-US&query=${query}` try{ const res = await fetch(url) const data = await res.json() console.log(data) }catch(err){ console.error(err) } }
|
成功獲得資料的結果如下,搜尋 “Jurassic Park” 會出現15筆資料。我們需要的資料存在 results 裡。
使用 useState 更新 query
在 input 內添加 value
, value
為 {query},即是用戶輸入的資訊。使用 useState
和useEffect
來更新 value 的值。const [query, setQuery] = useState("")
query 為value
變數;setQuery 用來更新 query 的值;useState
內的則是query的初始值。在 input 中添加 onChange
,使用 setQuery
更新 value
。
1 2 3 4 5 6 7 8 9
| const [query, setQuery] = useState("");
<input type = "text" name = "query" placeholder = "Goziila vs Kong..." value = {query} onChange ={(e) => setQuery(e.target.value)} />
|
使用 useState, useEffect 更新 movie (顯示給用戶的資料)
獲得搜尋資料之後,需要將這些資料渲染到瀏覽器,因此需要一個 array 來存取這些資料。一樣先使用 useState 要設置 movies array,再用 useEffect 更新。把 movies array 更新成搜尋後獲得的資料。
1 2 3 4 5 6 7 8 9 10 11
| const [movies, setMovies] = useState([])
try{ const res = await fetch(url) const data = await res.json() setMovies(data.results)
}catch(err){ console.error(err) } }
|
把搜尋結果呈現在畫面上
需要顯示在畫面上的資訊有:海報(poster_path),發行日期(release_date),電影簡介(overview),評分(vote_average)。可以用 movies array(裡面有剛才存進去的資料) 中調用我們需要的資料。使用 map 遍歷每一個搜尋結果,調用需要的資訊。
1 2 3 4 5 6 7 8 9 10
| <div className="card-list"> {movies.map(movie => ( <div className="card-info" key={movie.id}> <h3>{movie.title}</h3> //標題 <p>Release date: {movie.release_date}</p> <p>OverView: {movie.overview}</p> //簡介 <p>Rating: {movie.vote_average}</p> </div> ))} </div>
|
海報照片有固定的 url 格式,改變的只有結尾的 poster_path ,因此只要更換最後這個部分就可以獲得海報連接。某些原本就沒有海報照片的電影會無法顯示,可以使用兩種方法解決這個狀況:直接篩掉沒有海報的電影或是顯示代替圖案。
1
| https://image.tmdb.org/t/p/[width size]/[poster_path]
|
方法1 : 直接篩掉沒有海報的電影 (.fliter)
1 2 3 4 5 6 7 8 9 10
| <div className="card-list">
{movies.filter(movie => movie.poster_path).map(movie =>( <div className="card-info" key={movie.id}> <img src={`https://image.tmdb.org/t/p/w300/${movie.poster_path}`} alt={movie.title}/> </div> ))} </div>
|
方法2 : 顯示代替圖案 (onError)
1 2 3 4 5 6 7 8 9 10 11 12
| <div className="card-list"> {movies.map(movie => ( <div className="card-info" key={movie.id}> <img src={`https://image.tmdb.org/t/p/w300/${movie.poster_path}`} alt={movie.title} onError={(e)=>{e.target.onerror = null; e.target.src="https://i.postimg.cc/3RpfrHDh/photo.png"}} /> </div> ))} </div>
|
多個頁面
Movies 頁面抓取 10頁 Top rated 的電影。這裡頁面切換使用 material ui 的 Pagination 製作。在抓取需要的資料後,設置 page 的 state,默認為 1,即抓取第一頁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| import React, {useState, useEffect} from 'react' import { Container, MovieCard } from "../AllMovies/AllMovie_styles" import CustomPagination from "./CustomPagination" import ContentModal from '../ContentModal/ContentModal'
function AllMovies(){ const [ AllMovies, setAllMovies ] = useState([]) const [ page, setPage ] = useState(1)
const API_KEY = process.env.REACT_APP_API_KEY const API_URL = "https://api.themoviedb.org/3" const all_movie_url = `${API_URL}/movie/top_rated?api_key=${API_KEY}&language=en-US&page=${page}`
async function fetchAllMovies() { const res = await fetch(all_movie_url) const data = await res.json() setAllMovies(data) console.log(AllMovies.results) }
useEffect(() => { fetchAllMovies() window.scroll(0,0) }, [page])
return( <> <Container> <h2>What to watch</h2> {AllMovies.results && AllMovies.results.map((movie) => ( <MovieCard> . . . </MovieCard> ))} <CustomPagination setPage={setPage}/>
</Container> </> ) }
export default AllMovies
|
在 Pagination component 裡處理頁面更新 page 的 state,將 page 更換成點擊的頁數,資料便會根據該頁數抓取那頁的資料。
CustomPagination.js1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import React from "react" import Pagination from "@material-ui/lab/Pagination" import styled from "styled-components"
function CustomPagination({ setPage, numberOfPages = 10 }){ const handlePageChange = (page) => { setPage(page) window.scroll(0,0) }
return( <Pagination onChange={(e) => handlePageChange(e.target.textContent)} count={numberOfPages} shape="rounded" hideNextButton hidePrevButton /> ) }
export default CustomPagination
|
小結
Search 頁面是第一個做的頁面,因此記錄得比較詳細,其他部分只記錄了一些沒有用過的功能。Live Demo 中無法點擊的部分是還沒做好的功能,裡面也還有 bug 還沒修好。
由於個別頁面和功能都是獨立做的,因此相似的功能會重複寫,架構也不是那麼清楚。之後有時間會把它再重新整理一下,筆記也會再同步更新。若有錯誤或是可以寫得更好的地方再麻煩各位大大指點 :)
參考資料
Learn React in 1 Hour by Building a Movie Search App
Material UI Pagination