feat(curriculum): add weather app lab to cert (#56246)

Co-authored-by: Tom <20648924+moT01@users.noreply.github.com>
Co-authored-by: Dario-DC <105294544+Dario-DC@users.noreply.github.com>
This commit is contained in:
Ilenia
2024-12-09 17:14:49 +01:00
committed by GitHub
parent 6f5f1a520d
commit ea15720fe3
5 changed files with 903 additions and 1 deletions
+6 -1
View File
@@ -3325,7 +3325,12 @@
"ftmi": { "title": "275", "intro": [] },
"sgau": { "title": "276", "intro": [] },
"clak": { "title": "277", "intro": [] },
"fcom": { "title": "278", "intro": [] },
"lab-weather-app": {
"title": "Build a Weather App",
"intro": [
"In this lab you will build a Weather App using an API to fetch the data."
]
},
"ffpt": { "title": "279", "intro": [] },
"lrof": { "title": "280", "intro": [] },
"vyzp": { "title": "281", "intro": [] },
@@ -0,0 +1,9 @@
---
title: Introduction to the Build a Weather App
block: lab-weather-app
superBlock: full-stack-development
---
## Introduction to the Build a Weather App
In this lab you will build a Weather App using an API to fetch the data.
@@ -0,0 +1,11 @@
{
"name": "Build a Weather App",
"isUpcomingChange": true,
"usesMultifileEditor": true,
"dashedName": "lab-weather-app",
"superBlock": "full-stack-developer",
"challengeOrder": [{ "id": "66f12a88741aeb16b9246c59", "title": "Build a Weather App" }],
"helpCategory": "JavaScript",
"blockType": "lab",
"blockLayout": "link"
}
@@ -0,0 +1,876 @@
---
id: 66f12a88741aeb16b9246c59
title: Build a Weather App
challengeType: 14
dashedName: lab-weather-app
demoType: onClick
---
# --description--
**Objective:** Fulfill the user stories below and get all the tests to pass to complete the lab.
You will use a weather API. The output data has the following format:
```json
{
"weather": [
{
"main": "Clear",
"description": "clear sky",
"icon": "https://cdn.freecodecamp.org/weather-icons/01n.png" // icon representing the weather
}
],
"main": {
"temp": 2.62, // temperature in C
"feels_like": 0.84, // temperature in C
"temp_min": 1.72, // min temperature of the day in C
"temp_max": 3.49, // max temperature of the day in C
"pressure": 1010, // atmospheric pressure in hPa
"humidity": 81, // humidity in %
},
"visibility": 10000, // distance in meters
"wind": {
"speed": 1.79, // speed of the wind in m/s
"deg": 285, // orientation of the wind in degrees
"gust": 3.13 // gust speed in m/s
},
"name": "London",
}
```
**User Stories:**
1. You should have a `button` element with an `id` of `get-forecast`.
1. You should have a `select` element with seven `option` elements nested within it. The first option should have an empty string as its text and `value` attribute. The rest should have the follow for their text and values (with the value being lowercase):
- New York
- Los Angeles
- Chicago
- Paris
- Tokyo
- London
1. If no city is selected, pressing the button should do nothing.
1. If a city is selected, elements to show the weather should appear:
- You should have an `img` element with the id `weather-icon` for displaying the weather icon.
- You should have an element with the id `main-temperature` for displaying the main temperature.
- You should have an element with the id `feels-like` for displaying what the temperature feels like.
- You should have an element with the id `humidity` for displaying the amount of humidity in air.
- You should have an element with the id `wind` element for displaying the wind speed.
- You should have an element with the id `wind-gust` element for displaying the wind gust.
- You should have an element with the id `weather-main` element for displaying the main weather type.
- You should have an element with the id `location` element for displaying the current location.
1. You should have an asynchronous function named `getWeather` that fetches the weather information from the `https://weather-proxy.freecodecamp.rocks/api/city/<CITY>` API and returns it. Note that this API returns data using the metric system, that means m/s for wind speed, and Celsius for the temperature.
1. The `getWeather` asynchronous function should accept a city as its argument.
1. You should handle any errors that occur within the `getWeather` function and log them to the console.
1. You should have an asynchronous `showWeather` function that accepts a city as parameter.
1. The `showWeather` function should call the `getWeather` function to retrieve the weather data for the selected city from the dropdown.
1. If the `getWeather` function had an error, the app should only show an alert that says `Something went wrong, please try again later`.
1. If the data from `getWeather` are usable, the `showWeather` function should display the weather data in the corresponding elements. If a certain value from the API is `undefined`, you should write `N/A` in the corresponsing element.
NOTE: The tests will take time to complete. As long as you see `// running tests` in the console, they are being executed.
# --hints--
You should have a `button` element with an `id` of `get-forecast`.
```js
assert.exists(document.querySelector('button#get-forecast'));
```
You should have a `select` element.
```js
assert.exists(document.querySelector('select'));
```
Inside the `select` element the first child should be an `option` element with an empty `value` attribute.
```js
assert.exists(document.querySelector('select > option[value=""]'));
assert.strictEqual(document.querySelector('select > option:first-child').value, "");
```
Inside the `select` element there should be 6 `option` elements, one for each of the following cities: Paris, London, Tokyo, Los Angeles, Chicago, New York.
```js
["Paris", "London", "Tokyo", "Los Angeles", "Chicago", "New York"].forEach(city => {
const el = document.querySelector(`select > option[value="${city.toLowerCase()}"]`);
assert.exists(el);
assert.strictEqual(el.innerText.trim(), city)
});
```
You should have an `img` element with the id `weather-icon` for displaying the weather icon.
```js
async () => {
document.querySelector('select').value = "chicago";
document.querySelector('#get-forecast').click();
await new Promise(resolve => setTimeout(resolve, 800));
assert.exists(document.querySelector("img#weather-icon"));
}
```
You should have an element with the id `main-temperature` for displaying the main temperature.
```js
async () => {
document.querySelector('select').value = "london";
document.querySelector('#get-forecast').click();
await new Promise(resolve => setTimeout(resolve, 800));
assert.exists(document.querySelector("#main-temperature"));
}
```
You should have an element with the id `feels-like` for displaying what the temperature feels like.
```js
async () => {
document.querySelector('select').value = "london";
document.querySelector('#get-forecast').click();
await new Promise(resolve => setTimeout(resolve, 800));
assert.exists(document.querySelector("#feels-like"));
}
```
You should have an element with the id `humidity` for displaying the amount of humidity in air.
```js
async () => {
document.querySelector('select').value = "london";
document.querySelector('#get-forecast').click();
await new Promise(resolve => setTimeout(resolve, 800));
assert.exists(document.querySelector("#humidity"));
}
```
You should have an element with the id `wind` element for displaying the wind speed.
```js
async () => {
document.querySelector('select').value = "new york";
document.querySelector('#get-forecast').click();
await new Promise(resolve => setTimeout(resolve, 800));
assert.exists(document.querySelector("#wind"));
}
```
You should have an element with the id `wind-gust` element for displaying the wind gust.
```js
async () => {
document.querySelector('select').value = "new york";
document.querySelector('#get-forecast').click();
await new Promise(resolve => setTimeout(resolve, 800));
assert.exists(document.querySelector("#wind-gust"));
}
```
You should have an element with the id `weather-main` element for displaying the main weather type.
```js
async () => {
document.querySelector('select').value = "new york";
document.querySelector('#get-forecast').click();
await new Promise(resolve => setTimeout(resolve, 800));
assert.exists(document.querySelector("#weather-main"));
}
```
You should have an element with the id `location` element for displaying the current location.
```js
async () => {
document.querySelector('select').value = "new york";
document.querySelector('#get-forecast').click();
await new Promise(resolve => setTimeout(resolve, 800));
assert.exists(document.querySelector("#location"));
}
```
You should have a `showWeather` function.
```js
assert.isFunction(showWeather);
```
You should have a `getWeather` function.
```js
assert.isFunction(getWeather);
```
The `getWeather` function should accept a city as it's only argument and return the JSON from the Weather API.
```js
async () => {
try {
const city = "chicago";
const url = `https://weather-proxy.freecodecamp.rocks/api/city/${city}`;
const outputFunction = await getWeather(city);
const json = await fetch(url).then(data => data.json());
assert.deepEqual(outputFunction, json)
} catch (err) {
throw new Error(err);
}
}
```
The `showWeather` function should call the `getWeather` function to get the weather data.
```js
async () => {
try {
let flag = false;
const temp = getWeather;
getWeather = async (location) => {
flag = true;
return await fetch(`https://weather-proxy.freecodecamp.rocks/api/city/${location}`).then(data => data.json())
}
await showWeather("london");
getWeather = temp;
assert.isTrue(flag)
} catch (err) {
throw new Error(err);
}
}
```
When New York is selected the `showWeather` function should display the data from the API in the respective HTML elements. If the value from the API is `undefined`, you should show `N/A`.
```js
async () => {
try {
// function that construct an object with the id-value pairs that we expect in the page from an object
const helper = (wobj) => ({
"weather-icon": wobj.weather[0].icon,
"main-temperature": wobj.main.temp || 'N/A',
"feels-like": wobj.main.feels_like || 'N/A',
humidity: wobj.main.humidity || 'N/A',
wind: wobj.wind.speed || 'N/A',
"wind-gust": wobj.wind.gust || 'N/A',
"weather-main": wobj.weather[0].main || 'N/A',
location: wobj.name || 'N/A'
})
const city = "new york";
document.querySelector('select').value = city;
document.querySelector('#get-forecast').click();
// fetch the expected values from the API to confront
const body = await fetch(`https://weather-proxy.freecodecamp.rocks/api/city/${city}`).then(
data => data.json()
)
await new Promise(resolve => setTimeout(resolve, 800));
// construct the object from the data from the API
const extractedData = helper(body);
// check that all things are in place
for (const id in extractedData) {
if (id === "weather-icon") {
assert.equal(document.querySelector('#weather-icon').src, extractedData[id]);
} else {
assert.include(document.querySelector(`#${id}`).innerText.toLowerCase(), `${extractedData[id]}`.toLowerCase());
}
}
} catch (err) {
throw new Error(err);
}
}
```
When Chicago is selected the `showWeather` function should display the data from the API in the respective HTML elements. If the value from the API is `undefined`, you should show `N/A`.
```js
async () => {
try {
// function that construct an object with the id-value pairs that we expect in the page from an object
const helper = (wobj) => ({
"weather-icon": wobj.weather[0].icon,
"main-temperature": wobj.main.temp || 'N/A',
"feels-like": wobj.main.feels_like || 'N/A',
humidity: wobj.main.humidity || 'N/A',
wind: wobj.wind.speed || 'N/A',
"wind-gust": wobj.wind.gust || 'N/A',
"weather-main": wobj.weather[0].main || 'N/A',
location: wobj.name || 'N/A'
})
const city = "chicago";
document.querySelector('select').value = city;
document.querySelector('#get-forecast').click();
// fetch the expected values from the API to confront
const body = await fetch(`https://weather-proxy.freecodecamp.rocks/api/city/${city}`).then(
data => data.json()
)
await new Promise(resolve => setTimeout(resolve, 800));
// construct the object from the data from the API
const extractedData = helper(body);
// check that all things are in place
for (const id in extractedData) {
if (id === "weather-icon") {
assert.equal(document.querySelector('#weather-icon').src, extractedData[id]);
} else {
assert.include(document.querySelector(`#${id}`).innerText.toLowerCase(), `${extractedData[id]}`.toLowerCase());
}
}
} catch (err) {
throw new Error(err);
}
}
```
When London is selected the `showWeather` function should display the data from the API in the respective HTML elements. If the value from the API is `undefined`, you should show `N/A`.
```js
async () => {
try {
// function that construct an object with the id-value pairs that we expect in the page from an object
const helper = (wobj) => ({
"weather-icon": wobj.weather[0].icon,
"main-temperature": wobj.main.temp || 'N/A',
"feels-like": wobj.main.feels_like || 'N/A',
humidity: wobj.main.humidity || 'N/A',
wind: wobj.wind.speed || 'N/A',
"wind-gust": wobj.wind.gust || 'N/A',
"weather-main": wobj.weather[0].main || 'N/A',
location: wobj.name || 'N/A'
})
const city = "london";
document.querySelector('select').value = city;
document.querySelector('#get-forecast').click();
// fetch the expected values from the API to confront
const body = await fetch(`https://weather-proxy.freecodecamp.rocks/api/city/${city}`).then(
data => data.json()
)
await new Promise(resolve => setTimeout(resolve, 800));
// construct the object from the data from the API
const extractedData = helper(body);
// check that all things are in place
for (const id in extractedData) {
if (id === "weather-icon") {
assert.equal(document.querySelector('#weather-icon').src, extractedData[id]);
} else {
assert.include(document.querySelector(`#${id}`).innerText.toLowerCase(), `${extractedData[id]}`.toLowerCase());
}
}
} catch (err) {
throw new Error(err);
}
}
```
When Tokyo is selected the `showWeather` function should display the data from the API in the respective HTML elements. If the value from the API is `undefined`, you should show `N/A`.
```js
async () => {
try {
// function that construct an object with the id-value pairs that we expect in the page from an object
const helper = (wobj) => ({
"weather-icon": wobj.weather[0].icon,
"main-temperature": wobj.main.temp || 'N/A',
"feels-like": wobj.main.feels_like || 'N/A',
humidity: wobj.main.humidity || 'N/A',
wind: wobj.wind.speed || 'N/A',
"wind-gust": wobj.wind.gust || 'N/A',
"weather-main": wobj.weather[0].main || 'N/A',
location: wobj.name || 'N/A'
})
const city = "tokyo";
document.querySelector('select').value = city;
document.querySelector('#get-forecast').click();
// fetch the expected values from the API to confront
const body = await fetch(`https://weather-proxy.freecodecamp.rocks/api/city/${city}`).then(
data => data.json()
)
await new Promise(resolve => setTimeout(resolve, 800));
// construct the object from the data from the API
const extractedData = helper(body);
// check that all things are in place
for (const id in extractedData) {
if (id === "weather-icon") {
assert.equal(document.querySelector('#weather-icon').src, extractedData[id]);
} else {
assert.include(document.querySelector(`#${id}`).innerText.toLowerCase(), `${extractedData[id]}`.toLowerCase());
}
}
} catch (err) {
throw new Error(err);
}
}
```
When Los Angeles is selected the `showWeather` function should display the data from the API in the respective HTML elements. If the value from the API is `undefined`, you should show `N/A`.
```js
async () => {
try {
// function that construct an object with the id-value pairs that we expect in the page from an object
const helper = (wobj) => ({
"weather-icon": wobj.weather[0].icon,
"main-temperature": wobj.main.temp || 'N/A',
"feels-like": wobj.main.feels_like || 'N/A',
humidity: wobj.main.humidity || 'N/A',
wind: wobj.wind.speed || 'N/A',
"wind-gust": wobj.wind.gust || 'N/A',
"weather-main": wobj.weather[0].main || 'N/A',
location: wobj.name || 'N/A'
})
const city = "los angeles";
document.querySelector('select').value = city;
document.querySelector('#get-forecast').click();
// fetch the expected values from the API to confront
const body = await fetch(`https://weather-proxy.freecodecamp.rocks/api/city/${city}`).then(
data => data.json()
)
await new Promise(resolve => setTimeout(resolve, 800));
// construct the object from the data from the API
const extractedData = helper(body);
// check that all things are in place
for (const id in extractedData) {
if (id === "weather-icon") {
assert.equal(document.querySelector('#weather-icon').src, extractedData[id]);
} else {
assert.include(document.querySelector(`#${id}`).innerText.toLowerCase(), `${extractedData[id]}`.toLowerCase());
}
}
} catch (err) {
throw new Error(err);
}
}
```
If there is an error, your `getWeather` function should log the error to the console.
```js
const testArr = [];
const temp1 = fetch;
const temp2 = console.log;
const temp3 = console.error;
const temp4 = alert;
async () => {
try {
alert = () => {};
console.log = obj => {testArr.push(obj.toString())};
console.error = obj => {testArr.push(obj.toString())};
fetch = source => {throw new Error("This is a test error");}
await getWeather("chicago");
assert.include(testArr[0], "This is a test error");
assert.lengthOf(testArr, 1);
} finally {
fetch = temp1;
console.log = temp2;
console.error = temp3;
alert = temp4;
}
}
```
When Paris is selected the app should show an alert with `Something went wrong, please try again later`.
```js
const testArr = [];
const temp4 = alert;
async () => {
try {
alert = (msg) => {testArr.push(msg)};
const city = "paris";
document.querySelector('select').value = city;
document.querySelector('#get-forecast').click();
await new Promise(resolve => setTimeout(resolve, 800));
assert.include(testArr[0], "Something went wrong, please try again later");
assert.lengthOf(testArr, 1);
} finally {
alert = temp4;
}
}
```
# --seed--
## --seed-contents--
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Weather App</title>
</head>
<body>
</body>
</html>
```
```css
```
```js
```
# --solutions--
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Weather App</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="weather-info-wrap">
<div id="weather-info">
<div id="location"></div>
<div class="primary-info">
<div class="primary-info-left" id="main-temperature"></div>
<div class="primary-info-right">
<img id="weather-icon" >
<div id="weather-main"></div>
</div>
</div>
<hr >
<div class="secondary-info">
<div class="secondary-info-top">
<div id="humidity"></div>
<div id="feels-like"></div>
</div>
<div class="secondary-info-bottom">
<div class="secondary-info-bottom-left">
<div id="wind"></div>
<div id="wind-gust"></div>
</div>
<div class="secondary-info-bottom-right" id="compass">
<span id="compass-arrow">
<span class="arrow-head"></span>
</span>
<span class="compass-direction north"></span>
<span class="compass-direction east"></span>
<span class="compass-direction south"></span>
<span class="compass-direction west"></span>
</div>
</div>
</div>
</div>
</div>
<div class="btn-wrap">
<span>Weather for:</span>
<select id="location-selector">
<option value=""></option>
<option value="new york">New York</option>
<option value="los angeles">Los Angeles</option>
<option value="chicago">Chicago</option>
<option value="paris">Paris</option>
<option value="tokyo">Tokyo</option>
<option value="london">London</option>
</select>
<button id="get-forecast">Get Forecast</button>
</div>
<script type="text/javascript" src="script.js"></script>
</body>
</html>
```
```css
* {
box-sizing: border-box;
}
body {
background-color: #32a852;
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
row-gap: 40px;
font-size: 20px;
font-family: sans-serif;
}
.btn-wrap {
min-width: 700px;
display: flex;
justify-content: space-evenly;
column-gap: 20px;
border: 2px solid black;
padding: 20px;
border-radius: 10px;
background-color: #6cd488;
box-shadow: 6px 6px 6px rgba(0,0,0,0.5);
}
.btn-wrap > span {
text-transform: uppercase;
text-align: center;
margin: auto;
}
.btn-wrap > span::first-letter {
font-size: 1.3em
}
#location-selector {
width: 200px;
height: 50px;
font-size: 20px;
}
#get-forecast {
font-size: 20px;
height: 50px;
width: 200px;
}
.weather-info-wrap {
min-width: 700px;
max-width: 700px;
min-height: 300px;
padding: 20px 0px;
text-align: center;
background-color: #eee;
border-radius: 10px;
border: 2px solid black;
box-shadow: 6px 6px 6px rgba(0,0,0,0.5);
}
#weather-info {
display: none;
font-family: sans-serif;
padding: 20px 0;
}
#location {
font-size: 30px;
}
.primary-info {
display: flex;
justify-content: space-evenly;
margin: 20px;
font-size: 25px;
}
.primary-info-left {
display: flex;
align-items: center;
}
.primary-info-right {
display: flex;
justify-content: center;
align-items: center;
column-gap: 10px;
}
.secondary-info {
margin: 40px 20px 0 20px;
display: flex;
flex-direction: column;
row-gap: 20px;
}
.secondary-info-top {
display: flex;
align-items: center;
justify-content: space-evenly;
}
.secondary-info-bottom {
display: flex;
align-items: center;
justify-content: space-evenly;
margin-top: 40px;
}
.secondary-info-bottom-left {
display: flex;
align-items: center;
flex-direction: column;
row-gap: 20px;
justify-content: space-evenly;
}
.secondary-info-bottom-right {
width: 100px;
height: 100px;
border: 2px solid black;
border-radius: 50%;
position: relative;
}
#compass-arrow {
width: 3px;
height: 60px;
position: absolute;
background-color: black;
}
#compass-arrow {
width: 4px;
height: 70px;
top: 15%;
position: absolute;
background-color: black;
}
.arrow-head {
width: 0;
height: 0;
border: 8px solid transparent;
border-left-color: black;
position: absolute;
top: -15%;
left: -6px;
transform: rotate(-90deg);
}
.compass-direction {
position: absolute;
background-color: black;
}
.north, .south {
width: 3px;
height: 8px;
left: 50%;
}
.east, .west {
width: 8px;
height: 3px;
top: 50%;
}
.north {
top: -8px;
}
.south {
bottom: -8px;
}
.east {
right: -8px;
}
.west {
left: -8px;
}
```
```js
const getForecastBtn = document.getElementById('get-forecast');
const selectEl = document.getElementById('location-selector');
const weatherInfoEl = document.getElementById('weather-info');
const iconEl = document.getElementById('weather-icon');
const tempEl = document.getElementById('main-temperature');
const feelEl = document.getElementById('feels-like');
const humidityEl = document.getElementById('humidity');
const windEl = document.getElementById('wind');
const gustEl = document.getElementById('wind-gust');
const mainEl = document.getElementById('weather-main');
const locationEl = document.getElementById('location');
const arrowEl = document.getElementById('compass-arrow');
getForecastBtn.addEventListener('click', () =>
selectEl.value && showWeather(selectEl.value)
);
async function showWeather(city) {
const json = await getWeather(city);
const {
weather,
main:
{
temp,
feels_like,
humidity
},
wind: { speed, gust, deg = 0 },
name
} = json;
const { main, icon } = weather[0];
weatherInfoEl.style.display = 'block';
iconEl.src = icon || '';
tempEl.innerHTML = temp ? `${temp}&deg; C` : 'N/A';
feelEl.innerHTML = `Feels Like: ${feels_like ? `${feels_like}&deg; C` : 'N/A'}`;
humidityEl.innerHTML = `Humidity: ${humidity ? `${humidity}%` : 'N/A'}`;
windEl.innerHTML = `Wind: ${speed ? `${speed} m/s` : 'N/A'}`;
gustEl.innerHTML = `Gusts: ${gust ? `${gust} m/s` : 'N/A'}`;
mainEl.innerHTML = main || 'N/A';
locationEl.innerHTML = name || 'N/A';
arrowEl.style.transform = `rotate(${deg}deg)`;
}
async function getWeather(city) {
try {
const response = await fetch(`https://weather-proxy.freecodecamp.rocks/api/city/${city}`)
if (!response.ok) {
alert('Something went wrong, please try again later');
}
const json = await response.json();
return json;
} catch (error) {
console.error(error.message);
}
}
```
@@ -523,6 +523,7 @@
"blocks": [
{ "dashedName": "lecture-understanding-asynchronous-programming" },
{ "dashedName": "workshop-fcc-authors-page" },
{ "dashedName": "lab-weather-app" },
{ "dashedName": "review-asynchronous-javascript" },
{ "dashedName": "quiz-asynchronous-javascript" }
]