-
Notifications
You must be signed in to change notification settings - Fork 0
/
client.js
281 lines (232 loc) · 9.85 KB
/
client.js
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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
var Client = Client || function() {
// Debug
function log(message) {
/* Logs messages to debug console */
if (window.console && window.console.log) {
console.log(message);
} // else ignore if console unsupported
}
// Globals
var favorites = {}, // key is imdb id, value is title
lastMovieList = [], // lets us re-render the same movie list by saving a backup
searchCache = {}, // key is search term, value is search results
detailsCache = {}; // key is movie id, value is movie detail object
var API_URL = "http://www.omdbapi.com/?",
REFRESH_RATE_MS = 3000;
// API
function ajax(url, callback) {
/* Send AJAX GET to URL and pass result to callback */
var request = new XMLHttpRequest();
request.open('GET', url, true);
request.onload = function() { // Supporting only IE9+ to avoid more boilerplate
if (request.status >= 200 && request.status < 400) {
// Successful request + response
var data = JSON.parse(request.responseText);
callback(data);
} else {
// TODO: use onError callback?
throw new Error("Unable to complete AJAX request due to server error");
}
};
request.onerror = function() {
throw new Error("Unable to complete AJAX request due to connection error");
};
request.send();
}
function searchTitle(title, callback) {
/* Search the database for movie title and pass result to callback */
var query = "s=" + title;
ajax(API_URL + query, callback);
}
function searchId(id, callback) {
/* Get details about a particular movie */
var query = "i=" + id;
ajax(API_URL + query, callback);
}
// Favorites
function setFavorites(obj) {
/* update the local copy of favorites */
favorites = obj;
renderMovieList(); // we re-render because the model now includes a new favorite
}
function fetchFavorites() {
/* get favorites from server and update local copy */
ajax("/favorites", setFavorites);
}
function addFavorite(id, title) {
/* POST a new favorite to the Favorites API */
var request = new XMLHttpRequest(),
query = "oid=" + id + "&title=" + title; // TODO: Make this work with an object instead of querystring
request.open('POST', '/favorites', true);
request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
request.onload = function() {
if (request.status >= 200 && request.status < 400) {
var data = JSON.parse(request.responseText);
setFavorites(data); // the POST returns updated favorites list, which we can now save
} else {
throw new Error("Unable to complete AJAX request due to server error");
}
};
request.send(query);
}
function showAllFavorites() {
/* Render only favorites */
var movies = [];
for (var id in favorites) {
movies.push({imdbID: id, Title: favorites[id]}); // TODO: generalize property names instead of using api's
}
lastMovieList = movies;
renderMovieList(movies);
}
// Movie list
function getFavoriteLink(id, title) {
/* Constructs an anchor tag to favorite a movie title */
var $a = document.createElement("a");
$a.href = "#";
$a.innerHTML = "☺"; // smiley
$a.title = "Add favorite"; // this will display a tooltip on mouse hover
$a.addEventListener("click", function() {
addFavorite(id, title); // this works because the callback is a closure
});
$a = addClass($a, "fav-control");
return $a;
}
function addClass(element, className) {
/* Adds specified class to DOM element and returns it */
if (element.classList) { // have to do this two ways to support IE
element.classList.add(className);
} else {
element.className += " " + className;
}
return element;
}
function renderMovieList(movies) {
/*
Iterates through search results and appends a table row for each listing
Each row looks like:
<tr><td>Star Wars</td><td><a href="#">Favorite</a></td></tr>
*/
movies = movies || lastMovieList; // if nothing is passed in, just use last results
var $table = document.querySelectorAll("#movie-list")[0]; // TODO: use config instead of element id
if (movies.length) { // don't bother if there's nothing to show
clearMovieList(); // start blank
for (var i = 0; i < movies.length; i++) {
var $tr = document.createElement("tr"),
$titleTD = document.createElement("td"),
$favTD = document.createElement("td"),
title = movies[i].Title,
id = movies[i].imdbID,
$a = getFavoriteLink(id, title);
// first TD contains the title
$titleTD.innerHTML = title;
$titleTD.id = id;
$titleTD.addEventListener("click", handleTitleClick);
// second TD will contain an anchor tag
// TODO: Use span instead of anchor?
if (id in favorites) {
$a = addClass($a, "favorite");
}
$favTD.appendChild($a);
// add the TD's to the TR, and the TR to the table
$tr.appendChild($titleTD);
$tr.appendChild($favTD);
$table.appendChild($tr);
}
showMovieList();
}
}
function clearMovieList() {
var $table = document.querySelectorAll("#movie-list")[0];
$table.innerHTML = "";
$table.style.display = "none";
}
function showMovieList() {
var $table = document.querySelectorAll("#movie-list")[0];
$table.style.display = "table";
}
function searchForTerm(searchTerm) {
/* Check the cache or api for search results, and display */
var searchResults = [];
if (searchTerm) {
if (searchCache[searchTerm]) { // if the term exists in the cache, use it
searchResults = lastMovieList = searchCache[searchTerm];
renderMovieList(searchResults);
} else { // otherwise get the results from the api and add to the cache
searchTitle(searchTerm, function(data) {
if (data.Search) {
searchResults = data.Search;
searchCache[searchTerm] = lastMovieList = searchResults;
renderMovieList(searchResults);
}
});
}
} else {
clearMovieList();
hideMovieDetails();
}
}
// Movie details
function renderMovieDetails(data) {
/* Injects movie data into details area */
if (data) { // TODO: remove hardcoded ids
// Note: movie posters cannot be dynamically loaded because of imdb licensing
var title = document.getElementById("movie-title"),
year = document.getElementById("movie-year"),
rating = document.getElementById("movie-rating"),
director = document.getElementById("movie-director"),
cast = document.getElementById("movie-cast"),
plot = document.getElementById("movie-plot"),
details = document.getElementById("movie-details");
title.innerHTML = data.Title || "";
year.innerHTML = data.Year || "";
rating.innerHTML = data.Rated || "";
director.innerHTML = data.Director || "";
cast.innerHTML = data.Actors || "";
plot.innerHTML = data.Plot || "";
details.style.display = "block";
}
}
function showMovieDetails(movieId) {
/* Searches the cache, then the api for the clicked movie and renders the result */
if (movieId) {
if (movieId in detailsCache) { // use the cached details if they exist
renderMovieDetails(detailsCache[movieId]);
} else {
searchId(movieId, function(data) { // otherwise get from api and add to cache
renderMovieDetails(data);
detailsCache[movieId] = data;
});
}
} // TODO: something constructive if the id is missing
}
function hideMovieDetails() {
var details = document.getElementById("movie-details");
details.style.display = "none";
}
// Event handlers
function handleTitleClick(event) {
var movieId = event.target.id;
showMovieDetails(movieId);
}
function handleKeyup(event) {
var searchTerm = event.target.value;
if (searchTerm) { // search if the input is not empty
searchForTerm(searchTerm);
} else { // otherwise clear out the movie list
lastMovieList = [];
clearMovieList();
}
}
function handleAllFavoritesClick(event) {
event.preventDefault();
showAllFavorites();
}
// Init
var $input = document.getElementById("search-term");
var $allFavorites = document.getElementById("all-favorites");
$input.addEventListener("keyup", handleKeyup); // search while typing
fetchFavorites();
setInterval(fetchFavorites, REFRESH_RATE_MS); // Periodically download favorites in case other users have made changes
$allFavorites.addEventListener("click", handleAllFavoritesClick);
$input.focus();
}();