mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
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:
@@ -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"
|
||||
}
|
||||
+876
@@ -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}° 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);
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -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" }
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user