06-usePopcorn

本文最后更新于 2024年12月20日 下午

usePopcorn(记录观看过的电影并打分)

(一)实现页面及功能

页面:

初始页面

组件树:

组件树

功能:

1、输入电影名称搜索电影,搜索数量呈现在Numresults组件里,搜索结果呈现在左侧的Box组件里。

功能1

2、选择并查看电影简介及详细信息(上映时间,电影时长,电影类型,平均评分,主演人员,导演等)

功能2

此时组件树

3、对电影进行评分并添加在我的电影表单里

评分页面

添加到我观看单的页面

4、删除我的观看单里的影片并重新计算平均评分值和观看时间

删除前

删除后

(二)实现代码及解析

组件总览:

组件总览

组件分析:

(1)App

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
export default function App() {
const [query, setQuery] = useState("");
const [movies, setMovies] = useState([]);
const [watched, setWatched] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState("");
const [selectedId, setSelectedId] = useState(null);

// useEffect(function () {
// console.log("After initial render");
// }, []);

// useEffect(function () {
// console.log("After every render");
// });

// console.log("During render");

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

function handleCloseMovie() {
setSelectedId(null);
}

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

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

useEffect(
function () {
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 }
);
//网络问题,http响应状态码不是2xx
if (!res.ok)
throw new Error("Something went wrong with fetching movies");

//APIkey错误,API返回内容中Response字段为False
const data = await res.json();

if (data.Response === "False") {
setMovies([]);
throw new Error("Movie not found");
}
setMovies(data.Search);
} catch (err) {
if (err.name !== "AbortError") {
console.log(err.message);
setError(err.message);
}
} finally {
setIsLoading(false);
}
}

if (!query.length) {
setMovies([]);
setError("");
return; //return是为了不执行fetchMovies函数,提前退出function
}

handleCloseMovie();
fetchMovies();

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

return (
<>
<NavBar>
<Search query={query} setQuery={setQuery} />
<Numresults movies={movies} />
</NavBar>
<Main>
<Box>
{/* {isLoading ? <Loader /> : <MovieList movies={movies} />} */}
{isLoading && <Loader />}
{!isLoading && !error && (
<MovieList movies={movies} onSelectMovie={handleSelectMovie} />
)}
{error && <ErrorMessage message={error} />}
</Box>
<Box>
{selectedId ? (
<MovieDetails
selectedId={selectedId}
onCloseMovie={handleCloseMovie}
onAddWatched={handleAddWatched}
watched={watched}
/>
) : (
<>
<WatchedSummary watched={watched} />
<WatchedMovieList
watched={watched}
onDeleteWatched={handleDeleteWatched}
/>
</>
)}
</Box>
</Main>
</>
);
}

解释:

(2)NavBar

1
2
3
4
5
6
7
8
function NavBar({ children }) {
return (
<nav className="nav-bar">
<Logo />
{children}
</nav>
);
}

解释:

1
2
3
4
5
6
7
8
function Logo() {
return (
<div className="logo">
<span role="img">🍿</span>
<h1>usePopcorn</h1>
</div>
);
}

解释:

(4)Search

1
2
3
4
5
6
7
8
9
10
11
function Search({ query, setQuery }) {
return (
<input
className="search"
type="text"
placeholder="Search movies..."
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
);
}

解释:

(5)Numresults

1
2
3
4
5
6
7
function Numresults({ movies }) {
return (
<p className="num-results">
Found <strong>{movies.length}</strong> results
</p>
);
}

解释:

(6)Main

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

解释:

(7)Box

1
2
3
4
5
6
7
8
9
10
11
12
13
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>
);
}

解释:

(8)Loader

1
2
3
4
function Loader() {
return <p className="loader">Loading...</p>;
}

解释:

(9)ErrorMessage

1
2
3
4
5
6
7
8
function ErrorMessage({ message }) {
return (
<p className="error">
<span>🚫</span>
{message}
</p>
);
}

解释:

(10)MovieList

1
2
3
4
5
6
7
8
9
function MovieList({ movies, onSelectMovie }) {
return (
<ul className="list list-movies">
{movies?.map((movie) => (
<Movie movie={movie} onSelectMovie={onSelectMovie} />
))}
</ul>
);
}

解释:

(11)Movie

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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>
);
}

解释:

(12)MovieDetails

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
129
130
131
132
133
134
135
136
137
138
139
function MovieDetails({ selectedId, onCloseMovie, onAddWatched, watched }) {
const [movie, setMovie] = useState({});
const [isLoading, setIsLoading] = useState(false);
const [userRating, setUserRating] = useState("");

const isWatched = watched.map((movie) => movie.imdbID).includes(selectedId);
const watchedUserRating = watched.find(
(movie) => movie.imdbID === selectedId
)?.userRating;
const {
Title: title,
Year: year,
Poster: poster,
Runtime: runtime,
imdbRating,
Plot: plot,
Released: released,
Actors: actors,
Director: director,
Genre: genre,
} = movie;

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

useEffect(
function () {
function callback(e) {
if (e.code === "Escape") {
onCloseMovie();
}
}

document.addEventListener("keydown", callback);

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

useEffect(
function () {
async function getMovieDetails() {
setIsLoading(true);
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} 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={24}
onSetRating={setUserRating}
/>
{userRating > 0 && (
<button className="btn-add" onClick={handleAdd}>
+ Add to list
</button>
)}
</>
) : (
<p>
You rated with movie {watchedUserRating}
<span></span>
</p>
)}
</div>
<p>
<em>{plot}</em>
</p>
<p>Starring {actors}</p>
<p>Directed by {director}</p>
</section>
</>
)}
</div>
);
}

解释:

(13)WatchedSummary

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

解释:

(14)WatchedMovieList

1
2
3
4
5
6
7
8
9
10
11
12
13
function WatchedMovieList({ watched, onDeleteWatched }) {
return (
<ul className="list">
{watched.map((movie) => (
<WatchedMovie
movie={movie}
key={movie.imdbID}
onDeleteWatched={onDeleteWatched}
/>
))}
</ul>
);
}

解释:

(15)WatchedMovie

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

解释:

(16)StarRating

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
import { useState } from "react";
import PropTypes from "prop-types";

const containerStyle = {
display: "flex",
alignItems: "center",
gap: "16px",
};

const starContainerStyle = {
display: "flex",
};
//可重用组件:
StarRating.propTypes = {
maxRating: PropTypes.number,
defaultRating: PropTypes.number,
color: PropTypes.string,
size: PropTypes.number,
message: PropTypes.array,
className: PropTypes.string,
onSetRating: PropTypes.func,
};

export default function StarRating({
maxRating = 5,
color = "#fcc419",
size = 48,
className = "",
messages = [],
defaultRating = 0,
onSetRating,
}) {
const [rating, setRating] = useState(defaultRating);
const [tempRating, setTempRating] = useState(0);

function handleRating(rating) {
setRating(rating);
onSetRating(rating);
}

const textStyle = {
lineHeight: "1",
maigin: "0",
color,
fontSize: `${size / 1.5}px`,
};

return (
<div style={containerStyle} className={className}>
<div style={starContainerStyle}>
{Array.from({ length: maxRating }, (_, i) => (
<Star
key={i}
full={tempRating ? tempRating >= i + 1 : rating >= i + 1}
onRate={() => handleRating(i + 1)}
onHoverIn={() => setTempRating(i + 1)}
onHoverOut={() => setTempRating(0)}
color={color}
size={size}
/>
))}
</div>
<p style={textStyle}>
{messages.length === maxRating
? messages[tempRating ? tempRating - 1 : rating - 1]
: tempRating || rating || ""}
</p>
</div>
);
}

function Star({ onRate, full, onHoverIn, onHoverOut, color, size }) {
const starStyle = {
width: `${size}px`,
height: `${size}px`,
display: "block",
cursor: "pointer",
};

return (
<span
role="button"
style={starStyle}
onClick={onRate}
onMouseEnter={onHoverIn}
onMouseLeave={onHoverOut}
>
{full ? (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill={color}
stroke={color}
>
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
</svg>
) : (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke={color}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="{2}"
d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z"
/>
</svg>
)}
</span>
);
}

解释:


06-usePopcorn
http://sue-channing.github.io/2024/12/16/06-usePopcorn/
作者
Sue-Channing
发布于
2024年12月16日
许可协议