-
Notifications
You must be signed in to change notification settings - Fork 17
Glib-w2-UsingAPIs #60
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
## Test Summary | ||
|
||
**Mentors**: For more information on how to review homework assignments, please refer to the [Review Guide](https://github.com/HackYourFuture/mentors/blob/main/assignment-support/review-guide.md). | ||
|
||
### 3-UsingAPIs - Week2 | ||
|
||
| Exercise | Passed | Failed | ESLint | | ||
|-------------------|--------|--------|--------| | ||
| ex1-programmerFun | 5 | - | ✕ | | ||
| ex2-pokemonApp | 5 | - | ✕ | | ||
| ex3-rollAnAce | 7 | - | ✕ | | ||
| ex4-diceRace | 7 | - | ✕ | | ||
| ex5-vscDebug | - | - | ✕ | | ||
| ex6-browserDebug | - | - | ✕ | |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,44 +1,28 @@ | ||
/*------------------------------------------------------------------------------ | ||
Full description at: https://github.com/HackYourFuture/Assignments/blob/main/3-UsingAPIs/Week2/README.md#exercise-1-programmer-fun | ||
|
||
1. Complete the function `requestData()` using `fetch()` to make a request to | ||
the url passed to it as an argument. The function should return a promise. | ||
Make sure that the promise is rejected in case of HTTP or network errors. | ||
2. Notice that the function `main()` calls `requestData()`, passing it the url | ||
`https://xkcd.now.sh/?comic=latest`. Try and run the code in the browser and | ||
open the browser's console to inspect the data returned from the request. | ||
3. Next, complete the function `renderImage()` to render an image as an `<img>` | ||
element appended to the document's body, using the data returned from the API. | ||
4. Complete the function `renderError()` to render any errors as an `<h1>` | ||
element appended to the document's body. | ||
5. Refactor the `main()` function to use `async/await`. | ||
6. Test error handling, for instance, by temporarily changing the `.sh` in the | ||
url with `.shx`. There is no server at the modified url, therefore this | ||
should result in a network (DNS) error. | ||
------------------------------------------------------------------------------*/ | ||
function requestData(url) { | ||
// TODO return a promise using `fetch()` | ||
async function requestData(url) { | ||
const response = await fetch(url); | ||
if (!response.ok) throw new Error(`Failed to fetch data. Status: ${response.status}`); | ||
return response.json(); | ||
} | ||
|
||
function renderImage(data) { | ||
// TODO render the image to the DOM | ||
console.log(data); | ||
const imageElement = document.createElement('img'); | ||
imageElement.src = data.img; | ||
document.body.append(imageElement); | ||
} | ||
|
||
function renderError(error) { | ||
// TODO render the error to the DOM | ||
console.log(error); | ||
const errorElement = document.createElement('h1'); | ||
errorElement.textContent = `Error: ${error.message}`; | ||
document.body.append(errorElement); | ||
} | ||
|
||
// TODO refactor with async/await and try/catch | ||
function main() { | ||
requestData('https://xkcd.now.sh/?comic=latest') | ||
.then((data) => { | ||
renderImage(data); | ||
}) | ||
.catch((error) => { | ||
renderError(error); | ||
}); | ||
async function main() { | ||
try { | ||
const data = await requestData('https://xkcd.vercel.app/?comic=latest') | ||
renderImage(data); | ||
} catch (error) { | ||
renderError(error); | ||
} | ||
} | ||
|
||
window.addEventListener('load', main); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,38 +1,96 @@ | ||
/*------------------------------------------------------------------------------ | ||
Full description at: https://github.com/HackYourFuture/Assignments/blob/main/3-UsingAPIs/Week2/README.md#exercise-2-gotta-catch-em-all | ||
async function fetchData(url, errorContext) { | ||
try { | ||
const response = await fetch(url); | ||
if (!response.ok) throw new Error(`Status: ${response.status}`); | ||
return response.json(); | ||
} catch (error) { | ||
console.error(`Fetching ${errorContext} error.`, error); | ||
renderErrorMessage(`Fetching ${errorContext} failed. ${error.message}`); | ||
return null; | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm glad that you are clearly rendering the error message to the user. However, it's a bit strange to pass an "errorContext" to this function. Technically it works, but its a strange pattern. We should try to decouple the functions of fetching data from rendering to the UI. You can keep this function as generic as possible without even a try/catch. You already know the context in whichever function called this generic |
||
|
||
Complete the four functions provided in the starter `index.js` file: | ||
async function fetchAndPopulatePokemons() { | ||
const pokemonListData = await fetchData('https://pokeapi.co/api/v2/pokemon?limit=151', 'pokemon list'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. so if you wrap this in a try/catch, you already know the context of what is the reason you are calling the generic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for clarification 🙏 As far as I remember the whole idea to add this 'errorContext' parameter was the desire to reduce the amount of code by having less try/catch blocks. Maybe this pokemon exercise is not a perfect illustration, but what if we have much more functions that use fetchData? I assumed it's good idea to create universal error handler in parent fetch and get rid of all further try/catch blocks ( + describe renderErrorMessage() call only once, instead of doing it in every fetch. Not sure if it makes sense, but at that moment I thought that I'm actually implementing DRY principle :) |
||
if (!pokemonListData) return; | ||
const allPokemons = pokemonListData.results; | ||
const selectElement = document.getElementById('pokemon-select'); | ||
|
||
allPokemons.forEach(pokemon => { | ||
const optionElement = document.createElement('option'); | ||
optionElement.textContent = pokemon.name; | ||
optionElement.value = pokemon.url; | ||
selectElement.append(optionElement); | ||
}); | ||
|
||
`fetchData`: In the `fetchData` function, make use of `fetch` and its Promise | ||
syntax in order to get the data from the public API. Errors (HTTP or network | ||
errors) should be logged to the console. | ||
selectElement.disabled = false; | ||
} | ||
|
||
`fetchAndPopulatePokemons`: Use `fetchData()` to load the pokemon data from the | ||
public API and populate the `<select>` element in the DOM. | ||
|
||
`fetchImage`: Use `fetchData()` to fetch the selected image and update the | ||
`<img>` element in the DOM. | ||
async function fetchImage() { | ||
const selectElement = document.getElementById('pokemon-select'); | ||
const imageElement = document.getElementById('pokemon-image'); | ||
|
||
`main`: The `main` function orchestrates the other functions. The `main` | ||
function should be executed when the window has finished loading. | ||
toggleLoadingAnimation(true); | ||
|
||
Use async/await and try/catch to handle promises. | ||
const selectedPokemonData = await fetchData(selectElement.value, 'pokemon image'); | ||
if (!selectedPokemonData) return; | ||
|
||
Try and avoid using global variables. As much as possible, try and use function | ||
parameters and return values to pass data back and forth. | ||
------------------------------------------------------------------------------*/ | ||
function fetchData(/* TODO parameter(s) go here */) { | ||
// TODO complete this function | ||
imageElement.src = selectedPokemonData | ||
.sprites | ||
.other['official-artwork'] | ||
.front_default; | ||
|
||
imageElement.onload = () => { | ||
toggleLoadingAnimation(false); | ||
imageElement.classList.add('animate'); | ||
imageElement.addEventListener('animationend', () => { | ||
imageElement.classList.remove('animate'); | ||
}); | ||
}; | ||
} | ||
|
||
function fetchAndPopulatePokemons(/* TODO parameter(s) go here */) { | ||
// TODO complete this function | ||
function toggleLoadingAnimation(isSpinnerVisible) { | ||
const spinner = document.querySelector('.spinner'); | ||
spinner.classList.toggle('visible', isSpinnerVisible); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💯 |
||
} | ||
|
||
function fetchImage(/* TODO parameter(s) go here */) { | ||
// TODO complete this function | ||
function renderErrorMessage(message) { | ||
const imageContainer = document.getElementById('image-container'); | ||
const showImageButton = document.getElementById('show-image-button'); | ||
imageContainer.textContent = `🚫 ${message}`; | ||
showImageButton.disabled = true; | ||
} | ||
|
||
function generateLayout() { | ||
document.body.innerHTML = ` | ||
<div id='app'> | ||
<h1>Explore Pokemons</h1> | ||
<div id='actions-container'> | ||
<select id='pokemon-select' disabled> | ||
<option value='' disabled selected>Choose Pokemon</option> | ||
</select> | ||
<button id='show-image-button' disabled>Show Image</button> | ||
</div> | ||
<div id='image-container'> | ||
<div class="spinner"></div> | ||
<img id='pokemon-image'/> | ||
</div> | ||
</div> | ||
`; | ||
initEventListeners(); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice job |
||
|
||
function initEventListeners() { | ||
const selectElement = document.getElementById('pokemon-select'); | ||
const showImageButton = document.getElementById('show-image-button'); | ||
|
||
selectElement.addEventListener('change', () => showImageButton.disabled = !selectElement.value); | ||
showImageButton.addEventListener('click', fetchImage); | ||
} | ||
|
||
function main() { | ||
// TODO complete this function | ||
generateLayout(); | ||
fetchAndPopulatePokemons(); | ||
} | ||
|
||
window.addEventListener('load', main); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,160 @@ | ||
/* add your styling here */ | ||
* { | ||
box-sizing: border-box; | ||
margin: 0; | ||
padding: 0; | ||
} | ||
|
||
:root { | ||
--color-dark: rgb(11, 11, 11); | ||
--color-light: rgb(237, 237, 237); | ||
--hover-color: rgb(226, 226, 226); | ||
--error-color: rgb(225, 32, 32); | ||
--default-transition: all 0.2s ease-out; | ||
} | ||
|
||
body { | ||
height: 100vh; | ||
display: flex; | ||
justify-content: center; | ||
align-items: center; | ||
-webkit-font-smoothing: antialiased; | ||
font-family: | ||
'Inter', | ||
system-ui, | ||
-apple-system, | ||
sans-serif; | ||
background-color: var(--color-light); | ||
} | ||
|
||
#app { | ||
display: flex; | ||
flex-direction: column; | ||
justify-content: center; | ||
align-items: center; | ||
gap: 3rem; | ||
} | ||
|
||
#actions-container { | ||
display: flex; | ||
gap: 0.5rem; | ||
} | ||
|
||
#image-container { | ||
position: relative; | ||
width: 400px; | ||
aspect-ratio: 1; | ||
color: var(--error-color); | ||
font-size: 1.5rem; | ||
font-weight: 600; | ||
text-align: center; | ||
} | ||
|
||
#pokemon-image { | ||
width: 100%; | ||
display: block; | ||
visibility: visible; | ||
opacity: 1; | ||
transition: var(--default-transition); | ||
} | ||
|
||
#pokemon-image.animate { | ||
animation: imageOnLoadAnimation 0.25s ease-out; | ||
} | ||
|
||
.spinner { | ||
position: absolute; | ||
top: 40%; | ||
left: 40%; | ||
transform: translate(-50%, -50%); | ||
width: 60px; | ||
height: 60px; | ||
border: 5px solid rgba(255, 255, 255, 0.75); | ||
border-top: 5px solid var(--color-dark); | ||
border-radius: 50%; | ||
animation: spinnerAnimation 1s linear infinite; | ||
visibility: hidden; | ||
opacity: 0; | ||
transition: var(--default-transition); | ||
} | ||
|
||
.spinner.visible { | ||
visibility: visible; | ||
opacity: 1; | ||
transition: var(--default-transition); | ||
} | ||
|
||
@keyframes spinnerAnimation { | ||
0% { | ||
transform: rotate(0deg); | ||
} | ||
100% { | ||
transform: rotate(360deg); | ||
} | ||
} | ||
|
||
@keyframes imageOnLoadAnimation { | ||
0% { | ||
transform: scale(0.9); | ||
opacity: 0; | ||
} | ||
100% { | ||
transform: scale(1); | ||
opacity: 1; | ||
} | ||
} | ||
|
||
button, | ||
select { | ||
cursor: pointer; | ||
font-family: inherit; | ||
font-weight: 600; | ||
font-size: 1rem; | ||
border: 1px solid grey; | ||
border-radius: 12px; | ||
padding: 0.8rem; | ||
text-transform: capitalize; | ||
} | ||
|
||
button { | ||
padding: 0.6rem 1.5rem; | ||
transition: var(--default-transition); | ||
background-color: var(--color-dark); | ||
color: var(--color-light); | ||
} | ||
|
||
button:hover { | ||
background-color: rgb(17, 17, 17); | ||
color: var(--color-light); | ||
} | ||
|
||
button:active { | ||
opacity: 0.5; | ||
} | ||
|
||
select:disabled, | ||
button:disabled { | ||
cursor: not-allowed; | ||
opacity: 0.5; | ||
} | ||
|
||
select { | ||
min-width: 200px; | ||
-webkit-appearance: none; | ||
-moz-appearance: none; | ||
appearance: none; | ||
outline: none; | ||
background: url(https://api.iconify.design/akar-icons/chevron-down.svg) | ||
no-repeat; | ||
background-size: 1rem; | ||
background-position: 170px; | ||
transition: 0.2s ease-out; | ||
} | ||
|
||
select:hover { | ||
background-color: var(--hover-color); | ||
} | ||
|
||
h1 { | ||
font-size: 3rem; | ||
font-weight: 800; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tip: don't forget to also add an alt for images for accessibility
img.alt = 'image description'