The Story: Planning a Weekend Trip
Meera and her college friends in Delhi are planning a weekend trip. Some want Jaipur, some want Shimla. Before booking the bus, Meera wants to know one thing: will it be sunny, rainy or cold there this weekend?
In this project you will build a weather app that answers her question. She types a city name, and the app shows the current temperature, humidity, wind and a 3-day forecast with simple words and emoji. The data is real and live, from a free weather service called Open-Meteo, which needs no account and no API key.
This is your first project that talks to a server on the internet. You will use fetch, async/await and try...catch, which you met in the JavaScript Fetch API and JavaScript Async/Await lessons. You will also learn something every real app needs: showing a loading message and friendly errors when things go wrong.
You need an internet connection. From Step 2 onwards, every example calls the Open-Meteo servers. If you are offline, the examples cannot get weather data. In Step 6 you will make the app show a clear message when this happens.
How the App Gets the Weather
A weather service needs a location as numbers (latitude and longitude), not a city name. So the app makes two requests, one after the other:
| Request | We send | We get back |
|---|---|---|
| 1. Geocoding API | The city name, for example Jaipur | Its full name, country, latitude and longitude |
| 2. Forecast API | The latitude and longitude | Current weather and daily highs and lows |
Both answers come back as JSON, text that response.json() turns into a normal JavaScript object. For Jaipur, the first request returns an object with a results array. When a city does not exist, the results property is missing completely. Remember this: it is how we will detect “City not found”.
Step 1: The Layout
We start with HTML and CSS only: a search form, a status line for messages, a card for the current weather and a list for the forecast. The values are typed by hand so you can see the final design.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Weekend Weather</title>
<style>
body { font-family: system-ui, sans-serif; background: #e0f2fe; color: #0f172a; margin: 0; padding: 24px 16px; }
.app { max-width: 440px; margin: 0 auto; }
h1 { font-size: 1.4rem; margin: 0 0 4px; }
.sub { margin: 0 0 16px; color: #475569; }
form { display: flex; gap: 8px; }
input { flex: 1; padding: 10px; border: 1px solid #94a3b8; border-radius: 8px; font-size: 1rem; }
button { padding: 10px 16px; border: none; border-radius: 8px; background: #0369a1; color: #ffffff; font-size: 1rem; cursor: pointer; }
button:disabled { opacity: 0.6; cursor: wait; }
.status { min-height: 1.4em; color: #334155; }
.status.error { color: #b91c1c; font-weight: 600; }
.card { background: #ffffff; border-radius: 12px; padding: 16px 20px; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08); }
.card h2 { font-size: 1.1rem; margin: 0; }
.now { display: flex; align-items: center; gap: 12px; margin: 8px 0; }
.icon { font-size: 2.6rem; }
.temp { font-size: 2.6rem; font-weight: 700; }
.details { color: #475569; margin: 0; }
.forecast { list-style: none; padding: 0; margin: 16px 0 0; display: grid; gap: 8px; }
.forecast li { display: flex; justify-content: space-between; align-items: center; background: #ffffff; border-radius: 10px; padding: 10px 16px; }
[hidden] { display: none !important; }
</style>
</head>
<body>
<div class="app">
<h1>Weekend Weather</h1>
<p class="sub">Check the weather before you travel.</p>
<form id="search-form">
<input id="city-input" type="text" value="Jaipur" placeholder="Enter a city" aria-label="City name">
<button id="search-btn" type="submit">Search</button>
</form>
<p id="status" class="status"></p>
<section id="current" class="card">
<h2 id="place">Jaipur, India</h2>
<div class="now">
<span id="icon" class="icon">☀️</span>
<span id="temp" class="temp">31°C</span>
</div>
<p id="desc">Clear sky</p>
<p id="details" class="details">Humidity 40% · Wind 8 km/h</p>
</section>
<ul id="forecast" class="forecast">
<li><strong>Saturday</strong> <span>32° / 20°</span></li>
<li><strong>Sunday</strong> <span>31° / 19°</span></li>
</ul>
</div>
<script>
const form = document.querySelector('#search-form');
const cityInput = document.querySelector('#city-input');
const searchBtn = document.querySelector('#search-btn');
const statusEl = document.querySelector('#status');
const currentEl = document.querySelector('#current');
const forecastEl = document.querySelector('#forecast');
console.log('Weather app ready. City in the box:', cityInput.value); // Jaipur
</script>
</body>
</html>
The script selects every element we will need. The input already contains “Jaipur” through its value attribute, so you can test quickly with one tap. Note that the result card and the list will be empty containers from Step 2, filled by JavaScript.
Step 2: Find the City with the Geocoding API
Now the form does real work. Type a city and press Search. The status line shows the city’s latitude and longitude.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Weekend Weather</title>
<style>
body { font-family: system-ui, sans-serif; background: #e0f2fe; color: #0f172a; margin: 0; padding: 24px 16px; }
.app { max-width: 440px; margin: 0 auto; }
h1 { font-size: 1.4rem; margin: 0 0 4px; }
.sub { margin: 0 0 16px; color: #475569; }
form { display: flex; gap: 8px; }
input { flex: 1; padding: 10px; border: 1px solid #94a3b8; border-radius: 8px; font-size: 1rem; }
button { padding: 10px 16px; border: none; border-radius: 8px; background: #0369a1; color: #ffffff; font-size: 1rem; cursor: pointer; }
button:disabled { opacity: 0.6; cursor: wait; }
.status { min-height: 1.4em; color: #334155; }
.status.error { color: #b91c1c; font-weight: 600; }
.card { background: #ffffff; border-radius: 12px; padding: 16px 20px; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08); }
.card h2 { font-size: 1.1rem; margin: 0; }
.now { display: flex; align-items: center; gap: 12px; margin: 8px 0; }
.icon { font-size: 2.6rem; }
.temp { font-size: 2.6rem; font-weight: 700; }
.details { color: #475569; margin: 0; }
.forecast { list-style: none; padding: 0; margin: 16px 0 0; display: grid; gap: 8px; }
.forecast li { display: flex; justify-content: space-between; align-items: center; background: #ffffff; border-radius: 10px; padding: 10px 16px; }
[hidden] { display: none !important; }
</style>
</head>
<body>
<div class="app">
<h1>Weekend Weather</h1>
<p class="sub">Check the weather before you travel.</p>
<form id="search-form">
<input id="city-input" type="text" value="Jaipur" placeholder="Enter a city" aria-label="City name">
<button id="search-btn" type="submit">Search</button>
</form>
<p id="status" class="status"></p>
<section id="current" class="card" hidden>
<h2 id="place"></h2>
<div class="now">
<span id="icon" class="icon"></span>
<span id="temp" class="temp"></span>
</div>
<p id="desc"></p>
<p id="details" class="details"></p>
</section>
<ul id="forecast" class="forecast"></ul>
</div>
<script>
const form = document.querySelector('#search-form');
const cityInput = document.querySelector('#city-input');
const searchBtn = document.querySelector('#search-btn');
const statusEl = document.querySelector('#status');
const currentEl = document.querySelector('#current');
const forecastEl = document.querySelector('#forecast');
async function findCity(name) {
const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(name)}&count=1`;
const response = await fetch(url);
const data = await response.json();
if (!data.results) {
return null;
}
return data.results[0];
}
form.addEventListener('submit', async (event) => {
event.preventDefault();
const place = await findCity(cityInput.value.trim());
if (!place) {
statusEl.textContent = 'City not found';
return;
}
statusEl.textContent = `Found ${place.name}, ${place.country} at ${place.latitude.toFixed(2)}, ${place.longitude.toFixed(2)}`;
});
</script>
</body>
</html>
async function findCity(name)marks a function that can wait for slow work withawait. It always returns a Promise.encodeURIComponent(name)makes the name safe for a web address. A name like “Navi Mumbai” has a space, which must become%20in a URL.await fetch(url)sends the request and waits for the answer.await response.json()reads the body and turns it into an object.count=1in the URL asks for only the best match, so we takedata.results[0].- If
data.resultsis missing, we returnnulland show “City not found”. - The submit listener is also
async, so it canawait findCity(...).toFixed(2)shows the numbers with two decimal places.
Try “Shimla”, then a made-up word like “Abcxyz”.
Step 3: Get the Current Weather
With the latitude and longitude, we call the forecast API. The URL is long, so we build it in parts joined with +. Each part asks for something specific:
current=temperature_2m,relative_humidity_2m,wind_speed_10m,weather_code: the weather right now. “2m” and “10m” mean measured 2 metres and 10 metres above the ground.daily=weather_code,temperature_2m_max,temperature_2m_min: a summary for each day.timezone=auto: use the city’s own time zone for dates.forecast_days=3: only three days.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Weekend Weather</title>
<style>
body { font-family: system-ui, sans-serif; background: #e0f2fe; color: #0f172a; margin: 0; padding: 24px 16px; }
.app { max-width: 440px; margin: 0 auto; }
h1 { font-size: 1.4rem; margin: 0 0 4px; }
.sub { margin: 0 0 16px; color: #475569; }
form { display: flex; gap: 8px; }
input { flex: 1; padding: 10px; border: 1px solid #94a3b8; border-radius: 8px; font-size: 1rem; }
button { padding: 10px 16px; border: none; border-radius: 8px; background: #0369a1; color: #ffffff; font-size: 1rem; cursor: pointer; }
button:disabled { opacity: 0.6; cursor: wait; }
.status { min-height: 1.4em; color: #334155; }
.status.error { color: #b91c1c; font-weight: 600; }
.card { background: #ffffff; border-radius: 12px; padding: 16px 20px; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08); }
.card h2 { font-size: 1.1rem; margin: 0; }
.now { display: flex; align-items: center; gap: 12px; margin: 8px 0; }
.icon { font-size: 2.6rem; }
.temp { font-size: 2.6rem; font-weight: 700; }
.details { color: #475569; margin: 0; }
.forecast { list-style: none; padding: 0; margin: 16px 0 0; display: grid; gap: 8px; }
.forecast li { display: flex; justify-content: space-between; align-items: center; background: #ffffff; border-radius: 10px; padding: 10px 16px; }
[hidden] { display: none !important; }
</style>
</head>
<body>
<div class="app">
<h1>Weekend Weather</h1>
<p class="sub">Check the weather before you travel.</p>
<form id="search-form">
<input id="city-input" type="text" value="Jaipur" placeholder="Enter a city" aria-label="City name">
<button id="search-btn" type="submit">Search</button>
</form>
<p id="status" class="status"></p>
<section id="current" class="card" hidden>
<h2 id="place"></h2>
<div class="now">
<span id="icon" class="icon"></span>
<span id="temp" class="temp"></span>
</div>
<p id="desc"></p>
<p id="details" class="details"></p>
</section>
<ul id="forecast" class="forecast"></ul>
</div>
<script>
const form = document.querySelector('#search-form');
const cityInput = document.querySelector('#city-input');
const searchBtn = document.querySelector('#search-btn');
const statusEl = document.querySelector('#status');
const currentEl = document.querySelector('#current');
const forecastEl = document.querySelector('#forecast');
async function findCity(name) {
const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(name)}&count=1`;
const response = await fetch(url);
const data = await response.json();
if (!data.results) {
return null;
}
return data.results[0];
}
async function getWeather(lat, lon) {
const url = 'https://api.open-meteo.com/v1/forecast'
+ `?latitude=${lat}&longitude=${lon}`
+ '¤t=temperature_2m,relative_humidity_2m,wind_speed_10m,weather_code'
+ '&daily=weather_code,temperature_2m_max,temperature_2m_min'
+ '&timezone=auto&forecast_days=3';
const response = await fetch(url);
return response.json();
}
function showCurrent(place, current) {
document.querySelector('#place').textContent = `${place.name}, ${place.country}`;
document.querySelector('#temp').textContent = `${Math.round(current.temperature_2m)}°C`;
document.querySelector('#desc').textContent = `Weather code ${current.weather_code}`;
document.querySelector('#details').textContent =
`Humidity ${current.relative_humidity_2m}% · Wind ${Math.round(current.wind_speed_10m)} km/h`;
currentEl.hidden = false;
}
form.addEventListener('submit', async (event) => {
event.preventDefault();
const place = await findCity(cityInput.value.trim());
if (!place) {
statusEl.textContent = 'City not found';
currentEl.hidden = true;
return;
}
const data = await getWeather(place.latitude, place.longitude);
statusEl.textContent = '';
showCurrent(place, data.current);
});
</script>
</body>
</html>
showCurrent puts the values into the card with textContent and then sets currentEl.hidden = false to show it. Math.round turns 23.4 into 23, because nobody plans a trip around decimal degrees. The weather is still shown as a bare number, such as “Weather code 0”. Let us fix that next.
Step 4: Turn Weather Codes into Words and Emoji
Open-Meteo describes the sky with standard weather codes (called WMO codes). We only need a few groups:
| Code | Meaning | Emoji |
|---|---|---|
| 0 | Clear sky | ☀️ |
| 1 to 3 | Partly cloudy | ⛅ |
| 45, 48 | Fog | 🌫️ |
| 51 to 67 | Rain (drizzle and rain) | 🌧️ |
| 71 to 77 | Snow | ❄️ |
| 80 to 82 | Rain showers | 🌦️ |
| 95 to 99 | Thunderstorm | ⛈️ |
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Weekend Weather</title>
<style>
body { font-family: system-ui, sans-serif; background: #e0f2fe; color: #0f172a; margin: 0; padding: 24px 16px; }
.app { max-width: 440px; margin: 0 auto; }
h1 { font-size: 1.4rem; margin: 0 0 4px; }
.sub { margin: 0 0 16px; color: #475569; }
form { display: flex; gap: 8px; }
input { flex: 1; padding: 10px; border: 1px solid #94a3b8; border-radius: 8px; font-size: 1rem; }
button { padding: 10px 16px; border: none; border-radius: 8px; background: #0369a1; color: #ffffff; font-size: 1rem; cursor: pointer; }
button:disabled { opacity: 0.6; cursor: wait; }
.status { min-height: 1.4em; color: #334155; }
.status.error { color: #b91c1c; font-weight: 600; }
.card { background: #ffffff; border-radius: 12px; padding: 16px 20px; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08); }
.card h2 { font-size: 1.1rem; margin: 0; }
.now { display: flex; align-items: center; gap: 12px; margin: 8px 0; }
.icon { font-size: 2.6rem; }
.temp { font-size: 2.6rem; font-weight: 700; }
.details { color: #475569; margin: 0; }
.forecast { list-style: none; padding: 0; margin: 16px 0 0; display: grid; gap: 8px; }
.forecast li { display: flex; justify-content: space-between; align-items: center; background: #ffffff; border-radius: 10px; padding: 10px 16px; }
[hidden] { display: none !important; }
</style>
</head>
<body>
<div class="app">
<h1>Weekend Weather</h1>
<p class="sub">Check the weather before you travel.</p>
<form id="search-form">
<input id="city-input" type="text" value="Jaipur" placeholder="Enter a city" aria-label="City name">
<button id="search-btn" type="submit">Search</button>
</form>
<p id="status" class="status"></p>
<section id="current" class="card" hidden>
<h2 id="place"></h2>
<div class="now">
<span id="icon" class="icon"></span>
<span id="temp" class="temp"></span>
</div>
<p id="desc"></p>
<p id="details" class="details"></p>
</section>
<ul id="forecast" class="forecast"></ul>
</div>
<script>
const form = document.querySelector('#search-form');
const cityInput = document.querySelector('#city-input');
const searchBtn = document.querySelector('#search-btn');
const statusEl = document.querySelector('#status');
const currentEl = document.querySelector('#current');
const forecastEl = document.querySelector('#forecast');
function describeWeather(code) {
if (code === 0) return { text: 'Clear sky', icon: '☀️' };
if (code <= 3) return { text: 'Partly cloudy', icon: '⛅' };
if (code === 45 || code === 48) return { text: 'Fog', icon: '🌫️' };
if (code >= 51 && code <= 67) return { text: 'Rain', icon: '🌧️' };
if (code >= 71 && code <= 77) return { text: 'Snow', icon: '❄️' };
if (code >= 80 && code <= 82) return { text: 'Rain showers', icon: '🌦️' };
if (code >= 95 && code <= 99) return { text: 'Thunderstorm', icon: '⛈️' };
return { text: 'Mixed weather', icon: '🌥️' };
}
async function findCity(name) {
const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(name)}&count=1`;
const response = await fetch(url);
const data = await response.json();
if (!data.results) {
return null;
}
return data.results[0];
}
async function getWeather(lat, lon) {
const url = 'https://api.open-meteo.com/v1/forecast'
+ `?latitude=${lat}&longitude=${lon}`
+ '¤t=temperature_2m,relative_humidity_2m,wind_speed_10m,weather_code'
+ '&daily=weather_code,temperature_2m_max,temperature_2m_min'
+ '&timezone=auto&forecast_days=3';
const response = await fetch(url);
return response.json();
}
function showCurrent(place, current) {
const weather = describeWeather(current.weather_code);
document.querySelector('#place').textContent = `${place.name}, ${place.country}`;
document.querySelector('#icon').textContent = weather.icon;
document.querySelector('#temp').textContent = `${Math.round(current.temperature_2m)}°C`;
document.querySelector('#desc').textContent = weather.text;
document.querySelector('#details').textContent =
`Humidity ${current.relative_humidity_2m}% · Wind ${Math.round(current.wind_speed_10m)} km/h`;
currentEl.hidden = false;
}
form.addEventListener('submit', async (event) => {
event.preventDefault();
const place = await findCity(cityInput.value.trim());
if (!place) {
statusEl.textContent = 'City not found';
currentEl.hidden = true;
return;
}
const data = await getWeather(place.latitude, place.longitude);
statusEl.textContent = '';
showCurrent(place, data.current);
});
</script>
</body>
</html>
describeWeather(code) checks the groups from top to bottom and returns early as soon as one matches. Each answer is a small object with text and icon, so one call gives us both. The last line is a fallback for any code not in our list, so the app never shows a blank. Since code 0 is handled first, the check code <= 3 only catches 1, 2 and 3.
Step 5: Add the 3-Day Forecast
Meera needs the weekend, not just today. The daily part of the answer is a set of parallel arrays: position 0 of every array is today, position 1 is tomorrow, and so on.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Weekend Weather</title>
<style>
body { font-family: system-ui, sans-serif; background: #e0f2fe; color: #0f172a; margin: 0; padding: 24px 16px; }
.app { max-width: 440px; margin: 0 auto; }
h1 { font-size: 1.4rem; margin: 0 0 4px; }
.sub { margin: 0 0 16px; color: #475569; }
form { display: flex; gap: 8px; }
input { flex: 1; padding: 10px; border: 1px solid #94a3b8; border-radius: 8px; font-size: 1rem; }
button { padding: 10px 16px; border: none; border-radius: 8px; background: #0369a1; color: #ffffff; font-size: 1rem; cursor: pointer; }
button:disabled { opacity: 0.6; cursor: wait; }
.status { min-height: 1.4em; color: #334155; }
.status.error { color: #b91c1c; font-weight: 600; }
.card { background: #ffffff; border-radius: 12px; padding: 16px 20px; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08); }
.card h2 { font-size: 1.1rem; margin: 0; }
.now { display: flex; align-items: center; gap: 12px; margin: 8px 0; }
.icon { font-size: 2.6rem; }
.temp { font-size: 2.6rem; font-weight: 700; }
.details { color: #475569; margin: 0; }
.forecast { list-style: none; padding: 0; margin: 16px 0 0; display: grid; gap: 8px; }
.forecast li { display: flex; justify-content: space-between; align-items: center; background: #ffffff; border-radius: 10px; padding: 10px 16px; }
[hidden] { display: none !important; }
</style>
</head>
<body>
<div class="app">
<h1>Weekend Weather</h1>
<p class="sub">Check the weather before you travel.</p>
<form id="search-form">
<input id="city-input" type="text" value="Jaipur" placeholder="Enter a city" aria-label="City name">
<button id="search-btn" type="submit">Search</button>
</form>
<p id="status" class="status"></p>
<section id="current" class="card" hidden>
<h2 id="place"></h2>
<div class="now">
<span id="icon" class="icon"></span>
<span id="temp" class="temp"></span>
</div>
<p id="desc"></p>
<p id="details" class="details"></p>
</section>
<ul id="forecast" class="forecast"></ul>
</div>
<script>
const form = document.querySelector('#search-form');
const cityInput = document.querySelector('#city-input');
const searchBtn = document.querySelector('#search-btn');
const statusEl = document.querySelector('#status');
const currentEl = document.querySelector('#current');
const forecastEl = document.querySelector('#forecast');
function describeWeather(code) {
if (code === 0) return { text: 'Clear sky', icon: '☀️' };
if (code <= 3) return { text: 'Partly cloudy', icon: '⛅' };
if (code === 45 || code === 48) return { text: 'Fog', icon: '🌫️' };
if (code >= 51 && code <= 67) return { text: 'Rain', icon: '🌧️' };
if (code >= 71 && code <= 77) return { text: 'Snow', icon: '❄️' };
if (code >= 80 && code <= 82) return { text: 'Rain showers', icon: '🌦️' };
if (code >= 95 && code <= 99) return { text: 'Thunderstorm', icon: '⛈️' };
return { text: 'Mixed weather', icon: '🌥️' };
}
async function findCity(name) {
const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(name)}&count=1`;
const response = await fetch(url);
const data = await response.json();
if (!data.results) {
return null;
}
return data.results[0];
}
async function getWeather(lat, lon) {
const url = 'https://api.open-meteo.com/v1/forecast'
+ `?latitude=${lat}&longitude=${lon}`
+ '¤t=temperature_2m,relative_humidity_2m,wind_speed_10m,weather_code'
+ '&daily=weather_code,temperature_2m_max,temperature_2m_min'
+ '&timezone=auto&forecast_days=3';
const response = await fetch(url);
return response.json();
}
function showCurrent(place, current) {
const weather = describeWeather(current.weather_code);
document.querySelector('#place').textContent = `${place.name}, ${place.country}`;
document.querySelector('#icon').textContent = weather.icon;
document.querySelector('#temp').textContent = `${Math.round(current.temperature_2m)}°C`;
document.querySelector('#desc').textContent = weather.text;
document.querySelector('#details').textContent =
`Humidity ${current.relative_humidity_2m}% · Wind ${Math.round(current.wind_speed_10m)} km/h`;
currentEl.hidden = false;
}
function showForecast(daily) {
forecastEl.innerHTML = '';
daily.time.forEach((day, i) => {
const weather = describeWeather(daily.weather_code[i]);
const li = document.createElement('li');
const name = document.createElement('strong');
name.textContent = day;
const icon = document.createElement('span');
icon.textContent = `${weather.icon} ${weather.text}`;
const temps = document.createElement('span');
const max = Math.round(daily.temperature_2m_max[i]);
const min = Math.round(daily.temperature_2m_min[i]);
temps.textContent = `${max}° / ${min}°`;
li.append(name, icon, temps);
forecastEl.append(li);
});
}
form.addEventListener('submit', async (event) => {
event.preventDefault();
const place = await findCity(cityInput.value.trim());
if (!place) {
statusEl.textContent = 'City not found';
currentEl.hidden = true;
forecastEl.innerHTML = '';
return;
}
const data = await getWeather(place.latitude, place.longitude);
statusEl.textContent = '';
showCurrent(place, data.current);
showForecast(data.daily);
});
</script>
</body>
</html>
daily.time.forEach((day, i) => ...)walks through the dates. The indexilets us read the same day from the other arrays:daily.temperature_2m_max[i]anddaily.temperature_2m_min[i].- For each day we create a list item with the date, the emoji and words, and the high and low temperatures.
- We clear the list first with
forecastEl.innerHTML = '', so a second search does not add six rows.
The dates show as 2026-10-24 for now. In Step 7 we turn them into day names.
Step 6: Loading and Error States
So far, if the internet drops, the app fails silently and the console fills with red errors. A real app must tell the user what is happening. This step makes three improvements:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Weekend Weather</title>
<style>
body { font-family: system-ui, sans-serif; background: #e0f2fe; color: #0f172a; margin: 0; padding: 24px 16px; }
.app { max-width: 440px; margin: 0 auto; }
h1 { font-size: 1.4rem; margin: 0 0 4px; }
.sub { margin: 0 0 16px; color: #475569; }
form { display: flex; gap: 8px; }
input { flex: 1; padding: 10px; border: 1px solid #94a3b8; border-radius: 8px; font-size: 1rem; }
button { padding: 10px 16px; border: none; border-radius: 8px; background: #0369a1; color: #ffffff; font-size: 1rem; cursor: pointer; }
button:disabled { opacity: 0.6; cursor: wait; }
.status { min-height: 1.4em; color: #334155; }
.status.error { color: #b91c1c; font-weight: 600; }
.card { background: #ffffff; border-radius: 12px; padding: 16px 20px; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08); }
.card h2 { font-size: 1.1rem; margin: 0; }
.now { display: flex; align-items: center; gap: 12px; margin: 8px 0; }
.icon { font-size: 2.6rem; }
.temp { font-size: 2.6rem; font-weight: 700; }
.details { color: #475569; margin: 0; }
.forecast { list-style: none; padding: 0; margin: 16px 0 0; display: grid; gap: 8px; }
.forecast li { display: flex; justify-content: space-between; align-items: center; background: #ffffff; border-radius: 10px; padding: 10px 16px; }
[hidden] { display: none !important; }
</style>
</head>
<body>
<div class="app">
<h1>Weekend Weather</h1>
<p class="sub">Check the weather before you travel.</p>
<form id="search-form">
<input id="city-input" type="text" value="Jaipur" placeholder="Enter a city" aria-label="City name">
<button id="search-btn" type="submit">Search</button>
</form>
<p id="status" class="status"></p>
<section id="current" class="card" hidden>
<h2 id="place"></h2>
<div class="now">
<span id="icon" class="icon"></span>
<span id="temp" class="temp"></span>
</div>
<p id="desc"></p>
<p id="details" class="details"></p>
</section>
<ul id="forecast" class="forecast"></ul>
</div>
<script>
const form = document.querySelector('#search-form');
const cityInput = document.querySelector('#city-input');
const searchBtn = document.querySelector('#search-btn');
const statusEl = document.querySelector('#status');
const currentEl = document.querySelector('#current');
const forecastEl = document.querySelector('#forecast');
function describeWeather(code) {
if (code === 0) return { text: 'Clear sky', icon: '☀️' };
if (code <= 3) return { text: 'Partly cloudy', icon: '⛅' };
if (code === 45 || code === 48) return { text: 'Fog', icon: '🌫️' };
if (code >= 51 && code <= 67) return { text: 'Rain', icon: '🌧️' };
if (code >= 71 && code <= 77) return { text: 'Snow', icon: '❄️' };
if (code >= 80 && code <= 82) return { text: 'Rain showers', icon: '🌦️' };
if (code >= 95 && code <= 99) return { text: 'Thunderstorm', icon: '⛈️' };
return { text: 'Mixed weather', icon: '🌥️' };
}
async function getJSON(url) {
let response;
try {
response = await fetch(url);
} catch {
throw new Error('Network error. Check your internet connection and try again.');
}
if (!response.ok) {
throw new Error(`The weather service replied with status ${response.status}. Try again later.`);
}
return response.json();
}
async function findCity(name) {
const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(name)}&count=1`;
const data = await getJSON(url);
if (!data.results || data.results.length === 0) {
throw new Error('City not found. Check the spelling and try again.');
}
return data.results[0];
}
function getWeather(lat, lon) {
const url = 'https://api.open-meteo.com/v1/forecast'
+ `?latitude=${lat}&longitude=${lon}`
+ '¤t=temperature_2m,relative_humidity_2m,wind_speed_10m,weather_code'
+ '&daily=weather_code,temperature_2m_max,temperature_2m_min'
+ '&timezone=auto&forecast_days=3';
return getJSON(url);
}
function showCurrent(place, current) {
const weather = describeWeather(current.weather_code);
document.querySelector('#place').textContent = `${place.name}, ${place.country}`;
document.querySelector('#icon').textContent = weather.icon;
document.querySelector('#temp').textContent = `${Math.round(current.temperature_2m)}°C`;
document.querySelector('#desc').textContent = weather.text;
document.querySelector('#details').textContent =
`Humidity ${current.relative_humidity_2m}% · Wind ${Math.round(current.wind_speed_10m)} km/h`;
currentEl.hidden = false;
}
function showForecast(daily) {
forecastEl.innerHTML = '';
daily.time.forEach((day, i) => {
const weather = describeWeather(daily.weather_code[i]);
const li = document.createElement('li');
const name = document.createElement('strong');
name.textContent = day;
const icon = document.createElement('span');
icon.textContent = `${weather.icon} ${weather.text}`;
const temps = document.createElement('span');
const max = Math.round(daily.temperature_2m_max[i]);
const min = Math.round(daily.temperature_2m_min[i]);
temps.textContent = `${max}° / ${min}°`;
li.append(name, icon, temps);
forecastEl.append(li);
});
}
async function search(city) {
statusEl.className = 'status';
statusEl.textContent = 'Loading weather...';
searchBtn.disabled = true;
currentEl.hidden = true;
forecastEl.innerHTML = '';
try {
const place = await findCity(city);
const data = await getWeather(place.latitude, place.longitude);
showCurrent(place, data.current);
showForecast(data.daily);
statusEl.textContent = '';
} catch (error) {
statusEl.className = 'status error';
statusEl.textContent = error.message;
} finally {
searchBtn.disabled = false;
}
}
form.addEventListener('submit', (event) => {
event.preventDefault();
const city = cityInput.value.trim();
if (city === '') {
statusEl.className = 'status error';
statusEl.textContent = 'Please type a city name.';
return;
}
search(city);
});
</script>
</body>
</html>
- A helper,
getJSON(url).fetchthrows an error only when the request cannot be made at all (no internet, server unreachable). We catch that and throw our own clear message. If the server answers with an error status such as 500,response.okisfalse, and we throw a different message. - “City not found” becomes an error.
findCitynow usesthrow new Error(...)instead of returningnull. Every problem now travels the same road to onecatch. - One
search(city)function shows “Loading weather...”, disables the button so the user cannot send ten requests, and hides the old results. Thetryblock does the work. Thecatchblock showserror.messagein red. Thefinallyblock runs in both cases and turns the button back on.
The submit handler also refuses an empty box. Test the errors: search for “Abcxyz”, then turn off your Wi-Fi and search again.
Step 7: The Complete App
The last step adds two finishing touches: day names instead of dates, and an automatic search when the page opens, so the weather for Jaipur appears straight away.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Weekend Weather</title>
<style>
body { font-family: system-ui, sans-serif; background: #e0f2fe; color: #0f172a; margin: 0; padding: 24px 16px; }
.app { max-width: 440px; margin: 0 auto; }
h1 { font-size: 1.4rem; margin: 0 0 4px; }
.sub { margin: 0 0 16px; color: #475569; }
form { display: flex; gap: 8px; }
input { flex: 1; padding: 10px; border: 1px solid #94a3b8; border-radius: 8px; font-size: 1rem; }
button { padding: 10px 16px; border: none; border-radius: 8px; background: #0369a1; color: #ffffff; font-size: 1rem; cursor: pointer; }
button:disabled { opacity: 0.6; cursor: wait; }
.status { min-height: 1.4em; color: #334155; }
.status.error { color: #b91c1c; font-weight: 600; }
.card { background: #ffffff; border-radius: 12px; padding: 16px 20px; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08); }
.card h2 { font-size: 1.1rem; margin: 0; }
.now { display: flex; align-items: center; gap: 12px; margin: 8px 0; }
.icon { font-size: 2.6rem; }
.temp { font-size: 2.6rem; font-weight: 700; }
.details { color: #475569; margin: 0; }
.forecast { list-style: none; padding: 0; margin: 16px 0 0; display: grid; gap: 8px; }
.forecast li { display: flex; justify-content: space-between; align-items: center; background: #ffffff; border-radius: 10px; padding: 10px 16px; }
[hidden] { display: none !important; }
</style>
</head>
<body>
<div class="app">
<h1>Weekend Weather</h1>
<p class="sub">Check the weather before you travel.</p>
<form id="search-form">
<input id="city-input" type="text" value="Jaipur" placeholder="Enter a city" aria-label="City name">
<button id="search-btn" type="submit">Search</button>
</form>
<p id="status" class="status"></p>
<section id="current" class="card" hidden>
<h2 id="place"></h2>
<div class="now">
<span id="icon" class="icon"></span>
<span id="temp" class="temp"></span>
</div>
<p id="desc"></p>
<p id="details" class="details"></p>
</section>
<ul id="forecast" class="forecast"></ul>
</div>
<script>
const form = document.querySelector('#search-form');
const cityInput = document.querySelector('#city-input');
const searchBtn = document.querySelector('#search-btn');
const statusEl = document.querySelector('#status');
const currentEl = document.querySelector('#current');
const forecastEl = document.querySelector('#forecast');
function describeWeather(code) {
if (code === 0) return { text: 'Clear sky', icon: '☀️' };
if (code <= 3) return { text: 'Partly cloudy', icon: '⛅' };
if (code === 45 || code === 48) return { text: 'Fog', icon: '🌫️' };
if (code >= 51 && code <= 67) return { text: 'Rain', icon: '🌧️' };
if (code >= 71 && code <= 77) return { text: 'Snow', icon: '❄️' };
if (code >= 80 && code <= 82) return { text: 'Rain showers', icon: '🌦️' };
if (code >= 95 && code <= 99) return { text: 'Thunderstorm', icon: '⛈️' };
return { text: 'Mixed weather', icon: '🌥️' };
}
function dayName(dateText, index) {
if (index === 0) return 'Today';
const date = new Date(`${dateText}T00:00`);
return date.toLocaleDateString('en-IN', { weekday: 'long' });
}
async function getJSON(url) {
let response;
try {
response = await fetch(url);
} catch {
throw new Error('Network error. Check your internet connection and try again.');
}
if (!response.ok) {
throw new Error(`The weather service replied with status ${response.status}. Try again later.`);
}
return response.json();
}
async function findCity(name) {
const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(name)}&count=1`;
const data = await getJSON(url);
if (!data.results || data.results.length === 0) {
throw new Error('City not found. Check the spelling and try again.');
}
return data.results[0];
}
function getWeather(lat, lon) {
const url = 'https://api.open-meteo.com/v1/forecast'
+ `?latitude=${lat}&longitude=${lon}`
+ '¤t=temperature_2m,relative_humidity_2m,wind_speed_10m,weather_code'
+ '&daily=weather_code,temperature_2m_max,temperature_2m_min'
+ '&timezone=auto&forecast_days=3';
return getJSON(url);
}
function showCurrent(place, current) {
const weather = describeWeather(current.weather_code);
document.querySelector('#place').textContent = `${place.name}, ${place.country}`;
document.querySelector('#icon').textContent = weather.icon;
document.querySelector('#temp').textContent = `${Math.round(current.temperature_2m)}°C`;
document.querySelector('#desc').textContent = weather.text;
document.querySelector('#details').textContent =
`Humidity ${current.relative_humidity_2m}% · Wind ${Math.round(current.wind_speed_10m)} km/h`;
currentEl.hidden = false;
}
function showForecast(daily) {
forecastEl.innerHTML = '';
daily.time.forEach((day, i) => {
const weather = describeWeather(daily.weather_code[i]);
const li = document.createElement('li');
const name = document.createElement('strong');
name.textContent = dayName(day, i);
const icon = document.createElement('span');
icon.textContent = `${weather.icon} ${weather.text}`;
const temps = document.createElement('span');
const max = Math.round(daily.temperature_2m_max[i]);
const min = Math.round(daily.temperature_2m_min[i]);
temps.textContent = `${max}° / ${min}°`;
li.append(name, icon, temps);
forecastEl.append(li);
});
}
async function search(city) {
statusEl.className = 'status';
statusEl.textContent = 'Loading weather...';
searchBtn.disabled = true;
currentEl.hidden = true;
forecastEl.innerHTML = '';
try {
const place = await findCity(city);
const data = await getWeather(place.latitude, place.longitude);
showCurrent(place, data.current);
showForecast(data.daily);
statusEl.textContent = '';
} catch (error) {
statusEl.className = 'status error';
statusEl.textContent = error.message;
} finally {
searchBtn.disabled = false;
}
}
form.addEventListener('submit', (event) => {
event.preventDefault();
const city = cityInput.value.trim();
if (city === '') {
statusEl.className = 'status error';
statusEl.textContent = 'Please type a city name.';
return;
}
search(city);
});
// Show the weather for the city in the box as soon as the page opens.
search(cityInput.value.trim());
</script>
</body>
</html>
dayName(dateText, index)returns “Today” for the first day. For the others,new Date(`${dateText}T00:00`)creates a date at midnight in the local time. Without theT00:00part, the browser reads the date as UTC, which can show the wrong day in some countries.toLocaleDateString('en-IN', { weekday: 'long' })turns the date into a name like “Saturday”.- The last line,
search(cityInput.value.trim()), runs once when the page loads.
Real-Life Example: Jaipur or Shimla?
Meera opens the app. Jaipur appears first: clear sky and warm days. She types “Shimla” and presses Enter: cooler, with showers on Sunday. The group chooses Jaipur and packs caps and water bottles. The complete app from Step 7 is all she needed, and it worked with live data from a free public service.
Try these changes:
- Change
forecast_days=3toforecast_days=7for a full week. - Add
&temperature_unit=fahrenheitto the forecast URL and change the °C label. - Show the state too: the geocoding result has an
admin1property, such as “Rajasthan”.
Ideas to Take It Further
- Remember the last city with
localStorage, and search for it when the page opens. - Change the background colour based on the weather: blue for clear sky, grey for rain.
- Use your location with
navigator.geolocation.getCurrentPositionand skip the geocoding step. - Show several matches with
count=5, so the user can choose between cities with the same name.
Common Mistakes
- Forgetting
await. Without it, you get a Promise instead of the data, anddata.resultsisundefined. See the difference:
async function getNumber() {
return 42;
}
const withoutAwait = getNumber();
console.log(withoutAwait instanceof Promise); // true: a Promise, not the number
getNumber().then((value) => {
console.log(value); // 42
});
- Thinking
fetchfails on a 404 or 500. It does not.fetchonly rejects when there is no answer at all. Always checkresponse.ok. - Not handling a missing
resultsarray. Readingdata.results[0]whenresultsdoes not exist throws “Cannot read properties of undefined”. Check first, asfindCitydoes. - Putting city names into the URL without
encodeURIComponent. Names with spaces or special characters break the request. - Leaving the button enabled while loading. Impatient users click many times and old answers can arrive after new ones. Disable the button and enable it again in
finally.
Practice Task: Quick City Buttons
Meera’s friends keep checking the same four places. Add a row of quick city buttons so one tap shows the weather. The buttons are already in the HTML, each with a data-city attribute.
- Select the
#quickcontainer. - Use one click listener on the container (event delegation) and
closest('button'). - Put the city name into the search box, then call
search(). - Check that errors and loading still work.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Weekend Weather</title>
<style>
body { font-family: system-ui, sans-serif; background: #e0f2fe; color: #0f172a; margin: 0; padding: 24px 16px; }
.app { max-width: 440px; margin: 0 auto; }
h1 { font-size: 1.4rem; margin: 0 0 4px; }
.sub { margin: 0 0 16px; color: #475569; }
form { display: flex; gap: 8px; }
input { flex: 1; padding: 10px; border: 1px solid #94a3b8; border-radius: 8px; font-size: 1rem; }
button { padding: 10px 16px; border: none; border-radius: 8px; background: #0369a1; color: #ffffff; font-size: 1rem; cursor: pointer; }
button:disabled { opacity: 0.6; cursor: wait; }
.status { min-height: 1.4em; color: #334155; }
.status.error { color: #b91c1c; font-weight: 600; }
.card { background: #ffffff; border-radius: 12px; padding: 16px 20px; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08); }
.card h2 { font-size: 1.1rem; margin: 0; }
.now { display: flex; align-items: center; gap: 12px; margin: 8px 0; }
.icon { font-size: 2.6rem; }
.temp { font-size: 2.6rem; font-weight: 700; }
.details { color: #475569; margin: 0; }
.forecast { list-style: none; padding: 0; margin: 16px 0 0; display: grid; gap: 8px; }
.forecast li { display: flex; justify-content: space-between; align-items: center; background: #ffffff; border-radius: 10px; padding: 10px 16px; }
[hidden] { display: none !important; }
.quick { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
.quick button { background: #ffffff; color: #0369a1; border: 1px solid #0369a1; padding: 6px 12px; font-size: 0.9rem; }
</style>
</head>
<body>
<div class="app">
<h1>Weekend Weather</h1>
<p class="sub">Check the weather before you travel.</p>
<form id="search-form">
<input id="city-input" type="text" value="Jaipur" placeholder="Enter a city" aria-label="City name">
<button id="search-btn" type="submit">Search</button>
</form>
<div class="quick" id="quick">
<button type="button" data-city="Jaipur">Jaipur</button>
<button type="button" data-city="Shimla">Shimla</button>
<button type="button" data-city="Goa">Goa</button>
<button type="button" data-city="Munnar">Munnar</button>
</div>
<p id="status" class="status"></p>
<section id="current" class="card" hidden>
<h2 id="place"></h2>
<div class="now">
<span id="icon" class="icon"></span>
<span id="temp" class="temp"></span>
</div>
<p id="desc"></p>
<p id="details" class="details"></p>
</section>
<ul id="forecast" class="forecast"></ul>
</div>
<script>
const form = document.querySelector('#search-form');
const cityInput = document.querySelector('#city-input');
const searchBtn = document.querySelector('#search-btn');
const statusEl = document.querySelector('#status');
const currentEl = document.querySelector('#current');
const forecastEl = document.querySelector('#forecast');
function describeWeather(code) {
if (code === 0) return { text: 'Clear sky', icon: '☀️' };
if (code <= 3) return { text: 'Partly cloudy', icon: '⛅' };
if (code === 45 || code === 48) return { text: 'Fog', icon: '🌫️' };
if (code >= 51 && code <= 67) return { text: 'Rain', icon: '🌧️' };
if (code >= 71 && code <= 77) return { text: 'Snow', icon: '❄️' };
if (code >= 80 && code <= 82) return { text: 'Rain showers', icon: '🌦️' };
if (code >= 95 && code <= 99) return { text: 'Thunderstorm', icon: '⛈️' };
return { text: 'Mixed weather', icon: '🌥️' };
}
function dayName(dateText, index) {
if (index === 0) return 'Today';
const date = new Date(`${dateText}T00:00`);
return date.toLocaleDateString('en-IN', { weekday: 'long' });
}
async function getJSON(url) {
let response;
try {
response = await fetch(url);
} catch {
throw new Error('Network error. Check your internet connection and try again.');
}
if (!response.ok) {
throw new Error(`The weather service replied with status ${response.status}. Try again later.`);
}
return response.json();
}
async function findCity(name) {
const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(name)}&count=1`;
const data = await getJSON(url);
if (!data.results || data.results.length === 0) {
throw new Error('City not found. Check the spelling and try again.');
}
return data.results[0];
}
function getWeather(lat, lon) {
const url = 'https://api.open-meteo.com/v1/forecast'
+ `?latitude=${lat}&longitude=${lon}`
+ '¤t=temperature_2m,relative_humidity_2m,wind_speed_10m,weather_code'
+ '&daily=weather_code,temperature_2m_max,temperature_2m_min'
+ '&timezone=auto&forecast_days=3';
return getJSON(url);
}
function showCurrent(place, current) {
const weather = describeWeather(current.weather_code);
document.querySelector('#place').textContent = `${place.name}, ${place.country}`;
document.querySelector('#icon').textContent = weather.icon;
document.querySelector('#temp').textContent = `${Math.round(current.temperature_2m)}°C`;
document.querySelector('#desc').textContent = weather.text;
document.querySelector('#details').textContent =
`Humidity ${current.relative_humidity_2m}% · Wind ${Math.round(current.wind_speed_10m)} km/h`;
currentEl.hidden = false;
}
function showForecast(daily) {
forecastEl.innerHTML = '';
daily.time.forEach((day, i) => {
const weather = describeWeather(daily.weather_code[i]);
const li = document.createElement('li');
const name = document.createElement('strong');
name.textContent = dayName(day, i);
const icon = document.createElement('span');
icon.textContent = `${weather.icon} ${weather.text}`;
const temps = document.createElement('span');
const max = Math.round(daily.temperature_2m_max[i]);
const min = Math.round(daily.temperature_2m_min[i]);
temps.textContent = `${max}° / ${min}°`;
li.append(name, icon, temps);
forecastEl.append(li);
});
}
async function search(city) {
statusEl.className = 'status';
statusEl.textContent = 'Loading weather...';
searchBtn.disabled = true;
currentEl.hidden = true;
forecastEl.innerHTML = '';
try {
const place = await findCity(city);
const data = await getWeather(place.latitude, place.longitude);
showCurrent(place, data.current);
showForecast(data.daily);
statusEl.textContent = '';
} catch (error) {
statusEl.className = 'status error';
statusEl.textContent = error.message;
} finally {
searchBtn.disabled = false;
}
}
form.addEventListener('submit', (event) => {
event.preventDefault();
const city = cityInput.value.trim();
if (city === '') {
statusEl.className = 'status error';
statusEl.textContent = 'Please type a city name.';
return;
}
search(city);
});
// TODO 1: select the #quick container.
// TODO 2: listen for clicks on it, find the clicked button with closest(),
// put its data-city value in the input and call search().
// Show the weather for the city in the box as soon as the page opens.
search(cityInput.value.trim());
</script>
</body>
</html>
Show one possible answer (try it yourself first!)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Weekend Weather</title>
<style>
body { font-family: system-ui, sans-serif; background: #e0f2fe; color: #0f172a; margin: 0; padding: 24px 16px; }
.app { max-width: 440px; margin: 0 auto; }
h1 { font-size: 1.4rem; margin: 0 0 4px; }
.sub { margin: 0 0 16px; color: #475569; }
form { display: flex; gap: 8px; }
input { flex: 1; padding: 10px; border: 1px solid #94a3b8; border-radius: 8px; font-size: 1rem; }
button { padding: 10px 16px; border: none; border-radius: 8px; background: #0369a1; color: #ffffff; font-size: 1rem; cursor: pointer; }
button:disabled { opacity: 0.6; cursor: wait; }
.status { min-height: 1.4em; color: #334155; }
.status.error { color: #b91c1c; font-weight: 600; }
.card { background: #ffffff; border-radius: 12px; padding: 16px 20px; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08); }
.card h2 { font-size: 1.1rem; margin: 0; }
.now { display: flex; align-items: center; gap: 12px; margin: 8px 0; }
.icon { font-size: 2.6rem; }
.temp { font-size: 2.6rem; font-weight: 700; }
.details { color: #475569; margin: 0; }
.forecast { list-style: none; padding: 0; margin: 16px 0 0; display: grid; gap: 8px; }
.forecast li { display: flex; justify-content: space-between; align-items: center; background: #ffffff; border-radius: 10px; padding: 10px 16px; }
[hidden] { display: none !important; }
.quick { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
.quick button { background: #ffffff; color: #0369a1; border: 1px solid #0369a1; padding: 6px 12px; font-size: 0.9rem; }
</style>
</head>
<body>
<div class="app">
<h1>Weekend Weather</h1>
<p class="sub">Check the weather before you travel.</p>
<form id="search-form">
<input id="city-input" type="text" value="Jaipur" placeholder="Enter a city" aria-label="City name">
<button id="search-btn" type="submit">Search</button>
</form>
<div class="quick" id="quick">
<button type="button" data-city="Jaipur">Jaipur</button>
<button type="button" data-city="Shimla">Shimla</button>
<button type="button" data-city="Goa">Goa</button>
<button type="button" data-city="Munnar">Munnar</button>
</div>
<p id="status" class="status"></p>
<section id="current" class="card" hidden>
<h2 id="place"></h2>
<div class="now">
<span id="icon" class="icon"></span>
<span id="temp" class="temp"></span>
</div>
<p id="desc"></p>
<p id="details" class="details"></p>
</section>
<ul id="forecast" class="forecast"></ul>
</div>
<script>
const form = document.querySelector('#search-form');
const cityInput = document.querySelector('#city-input');
const searchBtn = document.querySelector('#search-btn');
const statusEl = document.querySelector('#status');
const currentEl = document.querySelector('#current');
const forecastEl = document.querySelector('#forecast');
function describeWeather(code) {
if (code === 0) return { text: 'Clear sky', icon: '☀️' };
if (code <= 3) return { text: 'Partly cloudy', icon: '⛅' };
if (code === 45 || code === 48) return { text: 'Fog', icon: '🌫️' };
if (code >= 51 && code <= 67) return { text: 'Rain', icon: '🌧️' };
if (code >= 71 && code <= 77) return { text: 'Snow', icon: '❄️' };
if (code >= 80 && code <= 82) return { text: 'Rain showers', icon: '🌦️' };
if (code >= 95 && code <= 99) return { text: 'Thunderstorm', icon: '⛈️' };
return { text: 'Mixed weather', icon: '🌥️' };
}
function dayName(dateText, index) {
if (index === 0) return 'Today';
const date = new Date(`${dateText}T00:00`);
return date.toLocaleDateString('en-IN', { weekday: 'long' });
}
async function getJSON(url) {
let response;
try {
response = await fetch(url);
} catch {
throw new Error('Network error. Check your internet connection and try again.');
}
if (!response.ok) {
throw new Error(`The weather service replied with status ${response.status}. Try again later.`);
}
return response.json();
}
async function findCity(name) {
const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(name)}&count=1`;
const data = await getJSON(url);
if (!data.results || data.results.length === 0) {
throw new Error('City not found. Check the spelling and try again.');
}
return data.results[0];
}
function getWeather(lat, lon) {
const url = 'https://api.open-meteo.com/v1/forecast'
+ `?latitude=${lat}&longitude=${lon}`
+ '¤t=temperature_2m,relative_humidity_2m,wind_speed_10m,weather_code'
+ '&daily=weather_code,temperature_2m_max,temperature_2m_min'
+ '&timezone=auto&forecast_days=3';
return getJSON(url);
}
function showCurrent(place, current) {
const weather = describeWeather(current.weather_code);
document.querySelector('#place').textContent = `${place.name}, ${place.country}`;
document.querySelector('#icon').textContent = weather.icon;
document.querySelector('#temp').textContent = `${Math.round(current.temperature_2m)}°C`;
document.querySelector('#desc').textContent = weather.text;
document.querySelector('#details').textContent =
`Humidity ${current.relative_humidity_2m}% · Wind ${Math.round(current.wind_speed_10m)} km/h`;
currentEl.hidden = false;
}
function showForecast(daily) {
forecastEl.innerHTML = '';
daily.time.forEach((day, i) => {
const weather = describeWeather(daily.weather_code[i]);
const li = document.createElement('li');
const name = document.createElement('strong');
name.textContent = dayName(day, i);
const icon = document.createElement('span');
icon.textContent = `${weather.icon} ${weather.text}`;
const temps = document.createElement('span');
const max = Math.round(daily.temperature_2m_max[i]);
const min = Math.round(daily.temperature_2m_min[i]);
temps.textContent = `${max}° / ${min}°`;
li.append(name, icon, temps);
forecastEl.append(li);
});
}
async function search(city) {
statusEl.className = 'status';
statusEl.textContent = 'Loading weather...';
searchBtn.disabled = true;
currentEl.hidden = true;
forecastEl.innerHTML = '';
try {
const place = await findCity(city);
const data = await getWeather(place.latitude, place.longitude);
showCurrent(place, data.current);
showForecast(data.daily);
statusEl.textContent = '';
} catch (error) {
statusEl.className = 'status error';
statusEl.textContent = error.message;
} finally {
searchBtn.disabled = false;
}
}
form.addEventListener('submit', (event) => {
event.preventDefault();
const city = cityInput.value.trim();
if (city === '') {
statusEl.className = 'status error';
statusEl.textContent = 'Please type a city name.';
return;
}
search(city);
});
const quickEl = document.querySelector('#quick');
quickEl.addEventListener('click', (event) => {
const button = event.target.closest('button');
if (!button) return;
cityInput.value = button.dataset.city;
search(button.dataset.city);
});
// Show the weather for the city in the box as soon as the page opens.
search(cityInput.value.trim());
</script>
</body>
</html>
Key Takeaways
- Many apps chain requests: first turn a city name into coordinates, then ask for the weather at those coordinates.
async/awaitmakes network code read from top to bottom. Wrap it intry...catch...finallyto show errors and always reset the page.fetchrejects only on network failure. Checkresponse.okfor server errors, and check the data itself (like a missingresults) for “not found”.- Map raw codes to friendly words and emoji with a small function that returns an object.
- Always show a loading message, disable buttons while waiting, and write error messages a user can act on.