-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.html
243 lines (226 loc) · 11.7 KB
/
index.html
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
<!doctype html>
<html lang="en">
<head>
<link rel='stylesheet' href='styles/styles.css'/>
</head>
<body>
<div class="description">
<h3>Vanilla JavaScript Exercise</h3>
<p><em>Goal:</em> Create a visual and interactive list, that can be sorted and filtered, of all the trips my wife and I have taken.</p>
<p><em>Summary:</em> My wife and I publish all the photos from our trips on SmugMug. Due to the nature of SmugMug, it's not easy to get a
chronological view of those trips. We also publish other material on the site that isn't always trip related. We wanted a way
to see them all in one place, and JavaScript was the answer. While SmugMug does allow for custom HTML and CSS creation, they
explicitly do not allow JavaScript, so this app had to be written and hosted somewhere else. Having previously written JS in
React.js and React Native, I was missing a lot of Vanilla JS (and HTML\CSS for that matter) fundamentals. It was time to really
invest in Vanilla JS because there are so many JS frameworks\abstractions and because JS is one of the most powerful languages
in the world..
</p>
<h4>What I learned, practiced, and reinforced</h4>
<ul>
<li>Linking external JavaScipt and CSS to the HTML page</li>
<li>Calling and using the <a href="https://api.smugmug.com/api/v2/doc" target="_blank">SmugMug API</a> (for albums)</li>
<li>Immediately being stopped by CORS
<ul>
<li><a href="https://developer.mozilla.org/en-us/docs/Web/HTTP/CORS" target="_blank">Understanding CORS</a></li>
<li>Getting around CORS</li>
<li>Testing different CORS proxies (thanks <a href="https://allorigins.win/" target="_blank">allOrigins</a>)</li>
</ul>
</li>
<li>JSON text formatter for practice (this seems like a good interview question that hits various built-in JavaScript features)
<ul>
<li>Using Object.keys()</li>
<li>Traversing the JSON object recursively</li>
<li>Using closures for controlling the current indent level</li>
<li>Using 'typeof' to inspect the data types</li>
</ul>
</li>
<li>Event listener to continue fetching albums
<ul>
<li>SmugMug limits 50 albums per request</li>
<li>SmugMug specifies the Next query request URL</li>
</ul>
</li>
<li>Regular Expressions to extract trip date, name, and category from the Album's URL</li>
<li>Date.prototype methods practice</li>
<li>Array.prototype methods for filtering, sorting, and iterating</li>
<li>Dynamically creating and populating a 'div' table using the DOM</li>
<li>Once all the albums are fetched, created more event listeners for:
<ul>
<li>Sorting the trips chronologically</li>
<li>Filtering the trips by name or category</li>
</ul>
</li>
<li>CSS Flexbox, selectors, 'box model' (ie. padding, border, margin), positioning, etc</li>
<li>Custom progress indicator while albums are fetched</li>
<li>Hide the SmugMug API key from frontend view source and open source repository
<ul>
<li><a href="https://github.com/chris-chapin/allOrigins" target="_blank">Forked the allOrigins repository</a></li>
<li><a href="https://glitch.com/edit/#!/orchid-evanescent-surfboard" target="_blank">Deployed a slightly modified version of allOrigins to glitch.com (free node.js hosting)</a></li>
</ul>
</li>
<li>Navigating the content on <a href="https://developer.mozilla.org/en-US/" target="_blank">MDN</a></li>
</ul>
<h4>Possible additions (requiring more structured album data in SmugMug)</h4>
<ul>
<li>Tags (ie. State, Wildnerness\Forest\Park name, prominent list)</li>
<li>Rating scale</li>
</ul>
</div>
<div class='tripTable'>
<h3 id='loadingIndicator' class='loadingIndicator'>Connecting to custom allOrigins CORS proxy hosted on glitch.com
(this will take 1-2 minutes if the service has gone to sleep)
</h3>
<div class='tripRow tripHeader'>
<div class="startDate cel">
<em>Sort by</em>
</div>
<div class='endDate cel'>
-
</div>
<div class="tripName cel">
<input id="tripsFilter" type="text" placeholder="Filter by trips (enter)"/>
</div>
<div class="tripActivity cel">
<input id="activityFilter" type="text" placeholder="Filter by activity (enter)"/>
</div>
</div>
<div class='tripRow tripHeader'>
<div id='sortStart' class="startDate cel">
<em>Start Date ↑</em>
</div>
<div class='endDate cel'>
<em>End Date</em>
</div>
<div class="tripName cel">
<em>Trip</em>
</div>
<div class="tripActivity cel">
<em>Activity</em>
</div>
</div>
<div id='tripsBody'>
<!-- this gets populated dynamically from fetch'd SmugMug data -->
</div>
</div>
<!-- footer -->
</body>
<script src="scripts/helper.js"></script>
<script>
let trips = [];
let tripsBody = document.getElementById(`tripsBody`);
let loadingIndicator = document.getElementById(`loadingIndicator`);
let loadingInterval;
let tripsTableState = {
sortDescending: false,
tripsFilterValue: ``,
activityFilterValue: ``,
fetchedAlbums: 0
};
// public allOrigins proxy - api.allorigins.win
// custom allOrigins proxy - orchid-evanescent-surfboard.glitch.me
tripsBody.addEventListener(`fetchAlbums`, event => {
fetch(`https://orchid-evanescent-surfboard.glitch.me/raw?url=${encodeURIComponent(`https://api.smugmug.com/${event.detail}`)}`, {
method: 'GET',
cache: 'no-cache',
headers: {
'Accept': 'application/json'
}
})
.then(response => {
if (response.ok) {
response.json().then(albums => {
if (albums.Response.Pages.Count > 0) {
parseAlbums(albums, trips);
}
// check for more albums
if (albums.Response.Pages.hasOwnProperty(`NextPage`)) {
tripsBody.dispatchEvent(new CustomEvent(`fetchAlbums`, {detail: albums.Response.Pages.NextPage}));
tripsTableState.fetchedAlbums += albums.Response.Pages.Count;
let dots = ``;
for (let i = loadingIndicator.innerHTML.length; i > 0; i--) {
if (loadingIndicator.innerHTML[i] === `.`) {
dots = `${dots}.`;
}
}
loadingIndicator.innerHTML = `Fetching albums ${tripsTableState.fetchedAlbums}/${albums.Response.Pages.Total}${dots}`;
resetTrips(trips);
}
else {
// finished fetching albums
// clean up loading indicator
clearInterval(loadingInterval);
loadingIndicator.style.display = `none`;
// add trips to page
sortTrips(trips, tripsTableState.sortDescending);
resetTrips(trips);
// create events for 'data analysis'
// event to change sort order of trip start date
document.getElementById(`sortStart`).addEventListener(`click`, event => {
tripsTableState.sortDescending = !tripsTableState.sortDescending;
tripsBody.dispatchEvent(new CustomEvent(`renderTripsTable`));
});
// event to filter trips list based on the trip name
document.getElementById(`tripsFilter`).addEventListener(`change`, event => {
tripsTableState.tripsFilterValue = event.target.value;
tripsBody.dispatchEvent(new CustomEvent(`renderTripsTable`));
});
// event to filter trips list based on the trip tag
document.getElementById(`activityFilter`).addEventListener(`change`, event => {
tripsTableState.activityFilterValue = event.target.value;
tripsBody.dispatchEvent(new CustomEvent(`renderTripsTable`));
});
}
});
}
else {
console.log(`fetch() failed with ${response.status}: ${response.statusText}`);
}
})
.catch(error => console.log(`fetch() threw with ${error}`))
});
tripsBody.addEventListener(`renderTripsTable`, event => {
// check if table needs to be re-sorted
let sortStartElem = document.getElementById(`sortStart`);
const upArrow = `\u2191`;
const downArrow = `\u2193`;
if (sortStartElem.innerHTML.includes(upArrow) && tripsTableState.sortDescending === true) {
// sort table oldest to newest
sortTrips(trips, tripsTableState.sortDescending);
sortStartElem.innerHTML = `<em>Start Date ${downArrow}</em>`;
}
else if (sortStartElem.innerHTML.includes(downArrow) && tripsTableState.sortDescending === false) {
// sort table newest to oldest
sortTrips(trips, tripsTableState.sortDescending);
sortStartElem.innerHTML = `<em>Start Date ${upArrow}</em>`;
}
let filteredTrips = [];
// check for filtering on trip name
if (tripsTableState.tripsFilterValue !== ``) {
filteredTrips = trips.filter(trip => trip.title.toLowerCase().includes(tripsTableState.tripsFilterValue.toLowerCase()));
}
// check for filtering on trip activity
if (tripsTableState.activityFilterValue !== ``) {
// additional filtering
if (filteredTrips.length > 0) {
let additionalFilteredTrips = filteredTrips.filter(trip => trip.activity.toLowerCase().includes(tripsTableState.activityFilterValue.toLowerCase()));
if (additionalFilteredTrips.length > 0) {
filteredTrips = additionalFilteredTrips;
}
}
else {
filteredTrips = trips.filter(trip => trip.activity.toLowerCase().includes(tripsTableState.activityFilterValue.toLowerCase()));
}
}
resetTrips(filteredTrips.length > 0 ? filteredTrips : trips);
});
tripsBody.dispatchEvent(new CustomEvent(`fetchAlbums`, {detail: `/api/v2/user/RadkaandChris!albums?start=1&count=50`}));
loadingInterval = setInterval(() => {
if (loadingIndicator.innerText.includes(`...`)) {
loadingIndicator.innerText = loadingIndicator.innerText.slice(0, loadingIndicator.innerText.length - 3);
}
else {
loadingIndicator.innerText = `${loadingIndicator.innerText}.`;
}
}, 500);
</script>
</html>