curriculum/challenges/english/blocks/lab-weather-app/66f12a88741aeb16b9246c59.md
In this lab, you'll build an app that provides weather information from different cities.
You will use a weather API. The output data has the following format:
{
"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"
}
Objective: Fulfill the user stories below and get all the tests to pass to complete the lab.
User Stories:
You should have a button element with an id of get-weather-btn.
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 following for their text and values (with the value being lowercase):
If no city is selected, pressing the button should do nothing.
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 for displaying the wind speed.
You should have an element with the id wind-gust for displaying the wind gust.
You should have an element with the id weather-main for displaying the main weather type.
You should have an element with the id location for displaying the current location.
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.
The getWeather asynchronous function should accept a city as its argument.
You should handle any errors that occur within the getWeather function and log them to the console.
You should have an asynchronous showWeather function that accepts a city as parameter.
The showWeather function should call the getWeather function to retrieve the weather data for the selected city from the dropdown.
If the getWeather function had an error, the app should only show an alert that says Something went wrong, please try again later.
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 corresponding element.
NOTE: The tests will take time to complete. As long as you see // running tests in the console, they are being executed.
window.fetch = (url) => {
const city = decodeURIComponent(url.split("/").pop());
const cityMap = {
"new york": {
coord: { lon: -74.0059, lat: 40.7127 },
weather: [
{
id: 804,
main: "Clouds",
description: "overcast clouds",
icon: "https://cdn.freecodecamp.org/weather-icons/04d.png",
},
],
base: "stations",
main: {
temp: 14.33,
feels_like: 13.2,
temp_min: 13.21,
temp_max: 15.54,
pressure: 1008,
humidity: 53,
sea_level: 1008,
grnd_level: 1007,
},
visibility: 10000,
wind: { speed: 3.6, deg: 40 },
clouds: { all: 100 },
dt: 1732123426,
sys: {
type: 1,
id: 4610,
country: "US",
sunrise: 1732103363,
sunset: 1732138471,
},
timezone: -18000,
id: 5128581,
name: "New York",
cod: 200,
},
"los angeles": {
coord: { lon: -118.2437, lat: 34.0522 },
weather: [
{
id: 800,
main: "Clear",
description: "clear sky",
icon: "https://cdn.freecodecamp.org/weather-icons/01d.png",
},
],
base: "stations",
main: {
temp: 17.24,
feels_like: 15.88,
temp_min: 14.84,
temp_max: 19.62,
pressure: 1023,
humidity: 33,
sea_level: 1023,
grnd_level: 1003,
},
visibility: 10000,
wind: { speed: 2.57, deg: 70 },
clouds: { all: 0 },
dt: 1732123664,
sys: {
type: 2,
id: 2075946,
country: "US",
sunrise: 1732113063,
sunset: 1732150008,
},
timezone: -28800,
id: 5368361,
name: "Los Angeles",
cod: 200,
},
chicago: {
coord: { lon: -87.6298, lat: 41.8781 },
weather: [
{
id: 802,
main: "Clouds",
description: "scattered clouds",
icon: "https://cdn.freecodecamp.org/weather-icons/03d.png",
},
],
base: "stations",
main: {
temp: 8.91,
feels_like: 4.91,
temp_min: 7.86,
temp_max: 9.44,
pressure: 1009,
humidity: 50,
sea_level: 1009,
grnd_level: 987,
},
visibility: 10000,
wind: { speed: 9.39, deg: 285, gust: 12.52 },
clouds: { all: 40 },
dt: 1732123645,
sys: {
type: 2,
id: 2010190,
country: "US",
sunrise: 1732106817,
sunset: 1732141557,
},
timezone: -21600,
id: 4887398,
name: "Chicago",
cod: 200,
},
london: {
coord: { lon: -0.1278, lat: 51.5074 },
weather: [
{
id: 800,
main: "Clear",
description: "clear sky",
icon: "https://cdn.freecodecamp.org/weather-icons/01n.png",
},
],
base: "stations",
main: {
temp: 2.62,
feels_like: 0.84,
temp_min: 1.72,
temp_max: 3.49,
pressure: 1010,
humidity: 81,
sea_level: 1010,
grnd_level: 1005,
},
visibility: 10000,
wind: { speed: 1.79, deg: 285, gust: 3.13 },
clouds: { all: 1 },
dt: 1732123462,
sys: {
type: 2,
id: 2075535,
country: "GB",
sunrise: 1732087658,
sunset: 1732118707,
},
timezone: 0,
id: 2643743,
name: "London",
cod: 200,
},
tokyo: {
coord: { lon: 139.6917, lat: 35.6895 },
weather: [
{
id: 501,
main: "Rain",
description: "moderate rain",
icon: "https://cdn.freecodecamp.org/weather-icons/10n.png",
},
],
base: "stations",
main: {
temp: 8.71,
feels_like: 5.38,
temp_min: 8.08,
temp_max: 9.81,
pressure: 1015,
humidity: 92,
sea_level: 1015,
grnd_level: 1014,
},
visibility: 7000,
wind: { speed: 6.69, deg: 330 },
rain: { "1h": 2.05 },
clouds: { all: 75 },
dt: 1732123711,
sys: {
type: 2,
id: 268395,
country: "JP",
sunrise: 1732137787,
sunset: 1732174284,
},
timezone: 32400,
id: 1850144,
name: "Tokyo",
cod: 200,
},
};
return Promise.resolve({
ok: city !== "paris",
json: () => Promise.resolve(cityMap[city]),
});
};
// 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 timeout = milliseconds =>
new Promise(resolve => {
return setTimeout(resolve, milliseconds)
});
You should have a button element with an id of get-weather-btn.
assert.exists(document.querySelector('button#get-weather-btn'));
You should have a select element.
assert.exists(document.querySelector('select'));
Inside the select element the first child should be an option element with an empty value attribute.
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.
['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.
document.querySelector('select').value = 'chicago';
document.querySelector('#get-weather-btn').click();
await new Promise(resolve => setTimeout(resolve, 1));
assert.exists(document.querySelector('img#weather-icon'));
You should have an element with the id main-temperature for displaying the main temperature.
document.querySelector('select').value = 'london';
document.querySelector('#get-weather-btn').click();
await new Promise(resolve => setTimeout(resolve, 1));
assert.exists(document.querySelector('#main-temperature'));
You should have an element with the id feels-like for displaying what the temperature feels like.
document.querySelector('select').value = 'london';
document.querySelector('#get-weather-btn').click();
await new Promise(resolve => setTimeout(resolve, 1));
assert.exists(document.querySelector('#feels-like'));
You should have an element with the id humidity for displaying the amount of humidity in air.
document.querySelector('select').value = 'london';
document.querySelector('#get-weather-btn').click();
await new Promise(resolve => setTimeout(resolve, 1));
assert.exists(document.querySelector('#humidity'));
You should have an element with the id wind for displaying the wind speed.
document.querySelector('select').value = 'new york';
document.querySelector('#get-weather-btn').click();
await new Promise(resolve => setTimeout(resolve, 1));
assert.exists(document.querySelector('#wind'));
You should have an element with the id wind-gust for displaying the wind gust.
document.querySelector('select').value = 'new york';
document.querySelector('#get-weather-btn').click();
await new Promise(resolve => setTimeout(resolve, 1));
assert.exists(document.querySelector('#wind-gust'));
You should have an element with the id weather-main for displaying the main weather type.
document.querySelector('select').value = 'new york';
document.querySelector('#get-weather-btn').click();
await new Promise(resolve => setTimeout(resolve, 1));
assert.exists(document.querySelector('#weather-main'));
You should have an element with the id location for displaying the current location.
document.querySelector('select').value = 'new york';
document.querySelector('#get-weather-btn').click();
await new Promise(resolve => setTimeout(resolve, 1));
assert.exists(document.querySelector('#location'));
You should have a showWeather function.
assert.isFunction(showWeather);
You should have a getWeather function.
assert.isFunction(getWeather);
The getWeather function should accept a city as its only argument and return the JSON from the Weather API.
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.
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);
}
The showWeather function should manage the case in which getWeather returns undefined.
const temp = getWeather;
try {
getWeather = () => {};
const city = 'london';
document.querySelector('select').value = city;
assert.doesNotThrow(showWeather);
assert.doesNotThrow(() => showWeather(city));
} finally {
getWeather = temp;
}
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.
try {
const city = 'new york';
document.querySelector('select').value = city;
document.querySelector('#get-weather-btn').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());
// construct the object from the data from the API
const extractedData = helper(body);
await timeout(1)
// 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.
try {
const city = 'chicago';
document.querySelector('select').value = city;
document.querySelector('#get-weather-btn').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());
// construct the object from the data from the API
const extractedData = helper(body);
await timeout(1)
// 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.
try {
const city = 'london';
document.querySelector('select').value = city;
document.querySelector('#get-weather-btn').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());
// construct the object from the data from the API
const extractedData = helper(body);
await timeout(1)
// 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.
try {
const city = 'tokyo';
document.querySelector('select').value = city;
document.querySelector('#get-weather-btn').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());
// construct the object from the data from the API
const extractedData = helper(body);
await timeout(1)
// 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.
try {
const city = 'los angeles';
document.querySelector('select').value = city;
document.querySelector('#get-weather-btn').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());
// construct the object from the data from the API
const extractedData = helper(body);
await timeout(1)
// 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.
const testArr = [];
const temp1 = fetch;
const temp2 = console.log;
const temp3 = console.error;
const temp4 = alert;
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.
const testArr = [];
const temp4 = alert;
try {
alert = msg => {
testArr.push(msg);
};
const city = 'paris';
document.querySelector('select').value = city;
document.querySelector('#get-weather-btn').click();
await new Promise(resolve => setTimeout(resolve, 1));
assert.include(testArr[0], 'Something went wrong, please try again later');
assert.lengthOf(testArr, 1);
} finally {
alert = temp4;
}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Weather App</title>
</head>
<body></body>
</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">
<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-weather-btn">Get Weather</button>
</div>
<script type="text/javascript" src="script.js"></script>
</body>
</html>
* {
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: 1.25em;
font-family: sans-serif;
}
.btn-wrap {
min-width: 100%;
display: flex;
justify-content: space-evenly;
align-items: center;
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);
}
@media (max-width: 768px) {
.btn-wrap {
flex-direction: column;
}
}
.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: 1.25em;
}
#get-weather-btn {
font-size: 1.25em;
height: 50px;
width: 200px;
}
.weather-info-wrap {
min-width: 100%;
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: 1.875em;
}
.primary-info {
display: flex;
justify-content: space-evenly;
margin: 20px;
font-size: 1.5625em;
}
.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;
}
const getWeatherBtn = document.getElementById('get-weather-btn');
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');
getWeatherBtn.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}° C` : 'N/A';
feelEl.innerHTML = `Feels Like: ${feels_like ? `${feels_like}° 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);
}
}