react课程9-usepopcorn-2

Last updated on August 28, 2024 pm

k

一、用户界面和主要功能

(1)界面

image-20240828110027563

(2)功能

1、调用API可以导入电影数据,搜索到电影并显示出基本信息

2、单击电影可以看到详细信息,并可以为它评分且加入已看电影的清单,也可以在清单中删除电影

3、右上角统计一共看了多少部电影以及评分和平均电影时间

4、离开界面再进去时数据不会丢失

二、代码

(1)App.js

从网站https://www.omdbapi.com/获取免费KEY后获取电影资源数据

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import { useEffect, useRef, useState } from "react";
import StarRating from "./StarRating";
import { useMovies } from "./useMovies";
import { useLocalStorageState } from "./useLocalStorageState";
import { useKey } from "./useKey";

const KEY = "c69cb565";

const average = (arr) =>
arr.reduce((acc, cur, i, arr) => acc + cur / arr.length, 0);

export default function App() {
const [query, setQuery] = useState("Flipped");
const [selectedId, setSelectedId] = useState(null);

const { movies, isLoading, error } = useMovies(query, handleCloseMovie);
const [watched, setWatched] = useLocalStorageState([], "watched");

function handleSelectMovie(id) {
setSelectedId((selectedId) => (id === selectedId ? null : id));
}

function handleCloseMovie() {
setSelectedId(null);
}

function hanleAddWatched(movie) {
setWatched((watched) => [...watched, movie]);
}

function handleDeleteWatched(id) {
setWatched((setWatched) => watched.filter((movie) => movie.imdbID !== id));
}

return (
<>
<NavBar>
<Search query={query} setQuery={setQuery} />
<NumResults movies={movies} />
</NavBar>

<Main>
<Box>
{isLoading && <Loader />}
{!isLoading && !error && (
<MovieList movies={movies} onSelectMovie={handleSelectMovie} />
)}
{error && <ErrorMessage message={error} />}
</Box>
<Box>
{selectedId ? (
<MovieDetails
selectedId={selectedId}
onCloseMovie={handleCloseMovie}
onAddWatched={hanleAddWatched}
watched={watched}
/>
) : (
<>
<WatchedSummary watched={watched} />
<WatchedMovieList
watched={watched}
onDeleteWatched={handleDeleteWatched}
/>
</>
)}
</Box>
</Main>
</>
);
}

Loader.js和Error.js

1
2
3
4
5
6
7
8
9
10
11
12
function Loader() {
return <p className="loader">Loading...</p>;
}

function ErrorMessage({ message }) {
return (
<p className="error">
<span>⚠️</span>
{message}
</p>
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function NavBar({ children }) {
return (
<nav className="nav-bar">
{" "}
<Logo />
{children}
</nav>
);
}

function Logo() {
return (
<div className="logo">
<span role="img">🍿</span>
<h1>usePopcorn</h1>
</div>
);
}
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
function Search({ query, setQuery }) {
// useEffect(function () {
// const el = document.querySelector(".search");
// el.focus();
// }, []);
const inputEl = useRef(null);

useKey("Enter", function () {
if (document.activeElement === inputEl.current) return;
inputEl.current.focus();
setQuery("");
});

return (
<input
className="search"
type="text"
placeholder="Search movies..."
value={query}
onChange={(e) => setQuery(e.target.value)}
ref={inputEl}
/>
);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function NumResults({ movies }) {
return (
<p className="num-results">
Found <strong>{movies.length}</strong> results
</p>
);
}

function Main({ children }) {
return <main className="main">{children}</main>;
}

function Box({ children }) {
const [isOpen, setIsOpen] = useState(true);
return (
<div className="box">
<button className="btn-toggle" onClick={() => setIsOpen((open) => !open)}>
{isOpen ? "–" : "+"}
</button>
{isOpen && children}
</div>
);
}
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

function MovieList({ movies, onSelectMovie }) {
return (
<ul className="list list-movies">
{movies?.map((movie) => (
<Movie movie={movie} key={movie.imdbID} onSelectMovie={onSelectMovie} />
))}
</ul>
);
}

function Movie({ movie, onSelectMovie }) {
return (
<li key={movie.imdbID} onClick={() => onSelectMovie(movie.imdbID)}>
<img src={movie.Poster} alt={`${movie.Title} poster`} />
<h3>{movie.Title}</h3>
<div>
<p>
<span>🗓</span>
<span>{movie.Year}</span>
</p>
</div>
</li>
);
}

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
function MovieDetails({ selectedId, onCloseMovie, onAddWatched, watched }) {
const [movie, setMovie] = useState({});
const [isLoading, setIsLoading] = useState(false);
const [userRating, setUserRating] = useState("");

const countRef = useRef(0);

useEffect(
function () {
if (userRating) countRef.current++;
},
[userRating]
);

const isWatched = watched.map((movie) => movie.imdbID).includes(selectedId);
const watchedUserRating = watched.find(
(movie) => movie.imdbID === selectedId
)?.userRating;

const {
Title: title,
Poster: poster,
Runtime: runtime,
imdbRating,
Plot: plot,
Released: released,
Actors: actors,
Director: director,
Genre: genre,
} = movie;

function handleAdd() {
const newWatchedMovie = {
imdbID: selectedId,
title,
poster,
imdbRating: Number(imdbRating),
runtime: Number(runtime.split(" ").at(0)),
userRating,
countRatingDecisions: countRef.current,
};
onAddWatched(newWatchedMovie);
onCloseMovie();
}

useKey("Escape", onCloseMovie);

useEffect(
function () {
setIsLoading(true);
async function getMovieDetails() {
const res = await fetch(
`http://www.omdbapi.com/?apikey=${KEY}&i=${selectedId}`
);
const data = await res.json();
setMovie(data);
setIsLoading(false);
}
getMovieDetails();
},
[selectedId]
);

useEffect(
function () {
if (!title) return;
document.title = `Movie|${title}`;

return function () {
document.title = "usePopcorn";
};
},
[title]
);

return (
<div className="details">
{isLoading ? (
<Loader />
) : (
<>
<header>
<button className="btn-back" onClick={onCloseMovie}>
&larr;
</button>
<img src={poster} alt={`Poster of ${movie}`} />
<div className="details-overview">
<h2>{title}</h2>
<p>
{released} &bull; {runtime}
</p>
<p>{genre}</p>
<p>
<span>🌟</span>
{imdbRating} IMDb rating
</p>
</div>
</header>
<section>
<div className="rating">
{!isWatched ? (
<>
<StarRating
maxRating={10}
size={36}
onSetRating={setUserRating}
/>
{userRating > 0 && (
<button className="btn-add" onClick={handleAdd}>
+ Add to list
</button>
)}
</>
) : (
<p>You rated with this movie {watchedUserRating}🌟</p>
)}
</div>
<p>
<em>{plot}</em>
</p>
<p>Starring: {actors}</p>
<p>Directed by: {director}</p>
</section>
</>
)}
</div>
);
}
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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
function WatchedSummary({ watched }) {
const avgImdbRating = average(watched.map((movie) => movie.imdbRating));
const avgUserRating = average(watched.map((movie) => movie.userRating));
const avgRuntime = average(watched.map((movie) => movie.runtime));
return (
<div className="summary">
<h2>Movies you watched</h2>
<div>
<p>
<span>#️⃣</span>
<span>{watched.length} movies</span>
</p>
<p>
<span>⭐️</span>
<span>{avgImdbRating.toFixed(2)}</span>
</p>
<p>
<span>🌟</span>
<span>{avgUserRating.toFixed(2)}</span>
</p>
<p>
<span></span>
<span>{avgRuntime} min</span>
</p>
</div>
</div>
);
}

function WatchedMovieList({ watched, onDeleteWatched }) {
return (
<ul className="list">
{watched.map((movie) => (
<WatchedMovie movie={movie} onDeleteWatched={onDeleteWatched} />
))}
</ul>
);
}

function WatchedMovie({ movie, onDeleteWatched }) {
return (
<li key={movie.imdbID}>
<img src={movie.poster} alt={`${movie.title} poster`} />
<h3>{movie.title}</h3>
<div>
<p>
<span>⭐️</span>
<span>{movie.imdbRating}</span>
</p>
<p>
<span>🌟</span>
<span>{movie.userRating}</span>
</p>
<p>
<span></span>
<span>{movie.runtime} min</span>
</p>
<button
className="btn-delete"
onClick={() => onDeleteWatched(movie.imdbID)}
>
x
</button>
</div>
</li>
);
}

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
49
50
51
52
53
54
55
56
57
58
59
60
61
import { useEffect, useState } from "react";

const KEY = "c69cb565";

export function useMovies(query, callback) {
const [movies, setMovies] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState("");

useEffect(
function () {
callback?.();

const controller = new AbortController();

async function fetchMovies() {
try {
setIsLoading(true);
setError("");
const res = await fetch(
`http://www.omdbapi.com/?apikey=${KEY}&s=${query}`,
{ signal: controller.signal }
);
//处理服务器返回了非2xx的状态码的情况,如404
if (!res.ok)
throw new Error("Something went wrong with fetching movies");

const data = await res.json();
if (data.Response === "False") throw new Error("Movie not found");
setMovies(data.Search);
setError("");
} catch (err) {
if (err.name !== "AbortError") {
console.error(err.message);
setError(err.message);
}
} finally {
setIsLoading(false);
}
//console.log(data.Search);
// .then((res) => res.json())
// .then((data) => setMovies(data.Search));
}
if (!query.length) {
setMovies([]);
setError("");
return;
}

//handleCloseMovie();
fetchMovies();

return function () {
controller.abort();
};
},
[query]
);
return { movies, isLoading, error };
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { useEffect } from "react";

export function useKey(key, action) {
useEffect(
function () {
function callback(e) {
if (e.code.toLowerCase() === key.toLowerCase()) action();
}

document.addEventListener("keydown", callback);

return function () {
document.removeEventListener("keydown", callback);
};
},
[action, key]
);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { useState, useEffect } from "react";

export function useLocalStorageState(initialState, key) {
const [value, setValue] = useState(function () {
const storedValue = localStorage.getItem(key);
return storedValue ? JSON.parse(storedValue) : initialState;
});

useEffect(
function () {
localStorage.setItem(key, JSON.stringify(value));
},
[value, key]
);

return [value, setValue];
}


react课程9-usepopcorn-2
http://example.com/2024/08/28/react课程9-usepopcorn-2/
Author
Yaodeer
Posted on
August 28, 2024
Licensed under