I'm making a WeatherApp. It uses the browser's location to get the GPS coordinates, then sends those to an API to get details about the location (e.g. city, country, etc.). The city is then sent to a weather API to get weather information, which is then displayed on the page - see the image below for a sample.
But I thinks there're a lot of ways to make it less clumsy. If you have some advice, you are welcome)
What's a better way to make "universal" widget?
While the code functions in the snippet, there is a security restriction. It does, however, work in this codepen. It should output data to widget: Widget on Codepen
// (1) There I get geolocation with two functions in case of success or error
navigator.geolocation.getCurrentPosition(success, error);
function success(position) {
const locationToken = '6efc940a8a8a31';
let lat = position.coords.latitude;
let lon = position.coords.longitude;
let locRequest = new XMLHttpRequest();
// (2) I use LocationIq to get the City name by geolocatioan coordinates
locRequest.open(
'GET',
`https://eu1.locationiq.com/v1/reverse.php?key=${locationToken}&lat=${lat}&lon=${lon}&format=json`,
true
);
locRequest.send();
locRequest.onreadystatechange = function() {
if (this.readyState != 4) return;
if (this.status != 200) {
alert('Error: ' + (this.status ? this.statusText : 'request aborted'));
return;
}
// (3) Okay, I got the city name and pass it to another function to get the weather in that city
let data = JSON.parse(this.responseText);
getWeather(data.address.city);
}
};
function error() {
console.log = "Unable to retrieve your location";
};
function getWeather(location) {
const weatherToken = 'bce14d2969489e270e45aaf51514f3a8';
let weatherLocation = location;
let weatherRequest = new XMLHttpRequest();
// (4) I send a request to OpenWeather using the City name
weatherRequest.open(
'GET',
`https://api.openweathermap.org/data/2.5/weather?q=${weatherLocation}&units=metric&appid=${weatherToken}`,
true
);
weatherRequest.send();
weatherRequest.onreadystatechange = function() {
if (this.readyState != 4) return;
if (this.status != 200) {
alert('Error: ' + (this.status ? this.statusText : 'request aborted'));
return;
}
let weather = JSON.parse(this.responseText);
// (5) and, finally, I pass all weather data to my widget
weatherWidget(weather);
}
}
function weatherWidget(data) {
let allData = data;
let currentLocation = document.querySelector('.js-currentLocation');
let temp = document.querySelector('.js-temp');
let weatherType = document.querySelector('.js-weatherType');
let weatherPreview = document.querySelector('.js-widgetPrev');
// (6) Here I pass necessary data to my Widget
currentLocation.innerHTML = allData.name;
temp.innerHTML = `${allData.main.temp}°C`;
weatherType.innerHTML = allData.weather[0].main;
let weatherPrevClass = allData.weather[0].main.toLowerCase();
weatherPreview.classList.add(weatherPrevClass);
}
:root {
--widget-size: 400px;
--widget-padding: 50px;
--widget-border-rad: 40px;
}
html,
body {
height: 100%;
}
body {
// background-color: #f9f8f8;
background-image: linear-gradient(to right bottom, #89f6df, #8ef9cb, #9dfab4, #b4f999, #d0f67f);
display: flex;
align-items: center;
justify-content: center;
font-family: 'Open Sans', sans-serif;
}
.widget {
position: relative;
width: var(--widget-size);
height: var(--widget-size);
color: #fff;
border-radius: var(--widget-border-rad);
background-color: rgba(255, 255, 255, .7);
box-shadow: 0 19px 38px rgba(0, 0, 0, 0.20), 0 15px 12px rgba(0, 0, 0, 0.15);
background-image: linear-gradient(to left top, #ffc94d, #ffb93a, #ffa729, #ff9518, #ff8208);
&__info {
position: absolute;
bottom: var(--widget-padding);
left: var(--widget-padding);
}
&__temp {
font-size: 6em;
line-height: 1em;
}
&__desc {
position: absolute;
top: var(--widget-padding);
left: var(--widget-padding);
font-size: 2.5em;
font-weight: 700;
}
&__currentLocation {
// text-align: center;
font-size: 2em;
}
&__prev {
position: absolute;
top: var(--widget-padding);
right: var(--widget-padding);
}
}
// ---> Sunny
.sun {
width: 100px;
height: 100px;
background-color: #fdde35;
border-radius: 50%;
position: relative;
}
.sun::after {
content: '';
display: block;
position: absolute;
width: inherit;
height: inherit;
border-radius: inherit;
background-color: inherit;
animation: sunshines 2s infinite;
}
@keyframes sunshines {
0% {
transform: scale(1);
opacity: 0.6;
}
100% {
transform: scale(1.5);
opacity: 0;
}
}
// ---> Cloudy
.clouds {
margin-top: 40px;
position: relative;
height: 60px;
width: 140px;
background-color: #fff;
border-radius: 30px;
animation: clouds 2.5s infinite ease-in-out;
&::after {
content: '';
display: block;
position: absolute;
width: 60px;
height: 60px;
background-color: inherit;
border-radius: 50%;
left: 25px;
top: -35px;
}
&::before {
content: '';
display: block;
position: absolute;
width: 50px;
height: 50px;
background-color: inherit;
border-radius: 50%;
left: 70px;
top: -25px;
}
}
@keyframes clouds {
50% {
transform: translateY(7px);
}
100% {
transform: translateY(0px);
}
}
<div class="widget">
<div class="widget__desc js-weatherType"></div>
<div class="widget__info">
<div class="widget__temp js-temp"></div>
<div class="widget__currentLocation js-currentLocation"></div>
</div>
<div class="widget__prev">
<div class="js-widgetPrev">
</div>
</div>
</div>
1 Answer 1
There appears to be a flaw with the function error
:
function error() { console.log = "Unable to retrieve your location"; };
That will re-assign that string literal to console.log
. Any subsequent calls to console.log()
would then throw a TypeError:
console.log is not a function
Presumably you meant to call that method and pass the string literal:
console.log("Unable to retrieve your location");
There is also console.error()
which could be used in this case:
console.error("Unable to retrieve your location");
When I try running the codepen on my office computer, the location data coming back from the locationIQ API does not contain a city
property in the address
property - see the sample below (also, it is about 3 kilometers away from my actual location - perhaps my ISP has equipment at that spot). You should consider an alternative property when this happens.
{
"place_id": "159393810",
"licence": "© LocationIQ.com CC BY 4.0, Data © OpenStreetMap contributors, ODbL 1.0",
"osm_type": "way",
"osm_id": "368233107",
"lat": "45.50757005",
"lon": "-122.6897144171",
"display_name": "1017, Southwest Myrtle Drive, Portland Heights, Southwest Hills, Multnomah County, Oregon, 97201, USA",
"address": {
"house_number": "1017",
"road": "Southwest Myrtle Drive",
"neighbourhood": "Portland Heights",
"suburb": "Southwest Hills",
"county": "Multnomah County",
"state": "Oregon",
"postcode": "97201",
"country": "USA",
"country_code": "us",
"town": "Southwest Hills"
},
"boundingbox": [
"45.5074436",
"45.5077148",
"-122.6899028",
"-122.6895124"
]
}
I was able to see weather information for my city using the postcode, which still displays the name of my city:
getWeather(data.address.postcode);
Though some would argue that a safer check would be to ensure that the property exists before using it.
I suggest you look into using Promises for XHR requests. There are APIs like the Fetch API, or others like reqwest and axios. Or as the MDN documentation illustrates, promises can be used to wrap up XHR code. Because you are using ecmascript-6 features, you could also consider async
and await
.
Many of the lines of code use let
for values that never get re-assigned. For example, the following two lines appear at the end of the callback function assigned to locRequest.onreadystatechange
:
let data = JSON.parse(this.responseText); getWeather(data.address.city);
Because data
is never re-assigned, const
would be appropriate and avoid any potential re-assignment. There are some who recommend the following:
- use
const
by default- only use
let
if rebinding is needed 1
Consider the DOM queries:
let currentLocation = document.querySelector('.js-currentLocation'); let temp = document.querySelector('.js-temp'); let weatherType = document.querySelector('.js-weatherType'); let weatherPreview = document.querySelector('.js-widgetPrev');
If those elements are really unique, then it would be advisable to update the HTML to use an id attribute for those single elements:
<div class="widget">
<div class="widget__desc" id="js-weatherType"></div>
<div class="widget__info">
<div class="widget__temp" id="js-temp"></div>
<div class="widget__currentLocation" id="js-currentLocation"></div>
</div>
<div class="widget__prev">
<div id="js-widgetPrev">
</div>
</div>
</div>
Then those elements can be selected by id:
const currentLocation = document.querySelector('#js-currentLocation');
const temp = document.querySelector('#js-temp');
const weatherType = document.querySelector('#js-weatherType');
const weatherPreview = document.querySelector('#js-widgetPrev');
And then use document.getElementById()
because it typically performs faster (see this performance comparison test) than document.querySelector()
, which supports many more selectors
const currentLocation = document.getElementById('js-currentLocation');
const temp = document.getElementById('js-temp');
const weatherType = document.getElementById('js-weatherType');
const weatherPreview = document.getElementById('js-widgetPrev');
https://api.openweathermap.org/data/2.5/weather
yields no results... perhaps because of theq
parameter in the query string isundefined
:q=undefined&units=metric&...
\$\endgroup\$