Skip to content

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .test-summary/TEST_SUMMARY.md
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 | - | - | ✕ |
50 changes: 17 additions & 33 deletions 3-UsingAPIs/Week2/assignment/ex1-programmerFun/index.js
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);

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'

}

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);
106 changes: 82 additions & 24 deletions 3-UsingAPIs/Week2/assignment/ex2-pokemonApp/index.js
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;
}
}

Choose a reason for hiding this comment

The 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 genericfetchData function. You can wrap it in a try/catch in the caller function. If it catches, call the renderErrorMessage there.


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

Choose a reason for hiding this comment

The 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 fetchData for, so here in the catch block you can then call renderErrorMessage

Copy link
Author

Choose a reason for hiding this comment

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

Choose a reason for hiding this comment

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

Choose a reason for hiding this comment

The 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);
161 changes: 160 additions & 1 deletion 3-UsingAPIs/Week2/assignment/ex2-pokemonApp/style.css
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;
}
Loading