Build a Weather Search App

Build a responsive weather search app with JavaScript and live Open-Meteo data.

Introduction

30 Second Summary

Checking the weather should take seconds. A vague place name or failed request can make a simple search feel unreliable.

In this project, you will build a responsive weather search app with HTML, CSS, and JavaScript. You will connect it to Open-Meteo, update the page with live conditions, and handle searches that return no city.

What You'll Build

You will enter a city in Microsoft Edge, press Search, and watch the weather card update with that location's current conditions.

By the end of this project, you'll have:

  • A working city search that responds to the keyboard or Search button without reloading the page.
  • A live weather card that shows the matched city, country, temperature, apparent temperature, condition, and wind speed.
  • A responsive interface that adapts to narrow screens while showing clear loading, success, and unknown-city states.
  • Secret Mission: Add a Celsius and Fahrenheit toggle that requests fresh weather data for the active city.

Are there any prerequisites?

You only need Visual Studio Code, Microsoft Edge, and internet access on Windows. Open-Meteo requires no account or API key for this non-commercial learning project.

Before We Start

Before any hands-on work, take a moment to define the purpose of Weather Search. Your reason for building it gives every web concept ahead a clear purpose.

Set Up the Three-File App

Weather Search begins as three small files in a single folder. This gives each part of the browser app a clear home.

A minimal structure keeps HTML, CSS, and JavaScript visible without a framework. You'll create it in Visual Studio Code before confirming the first heading in Microsoft Edge.

In this step, get ready to:
  • Create the weather-search folder in Windows Documents.
  • Build the linked three-file app skeleton.
  • Confirm the setup heading in Microsoft Edge.
Create the project folder

Windows File Explorer gives the project a stable location inside Documents. Visual Studio Code can then treat the folder as one workspace.

  • Press the Windows key to open Windows search.
  • Type File Explorer into the search field.
  • Press Enter to open File Explorer.
  • Select Documents in the left navigation.

You should now see the contents of your Windows Documents folder.

  • Right-click an empty area inside Documents.
  • Select New from the menu.
  • Select Folder.
  • Enter weather-search as the folder name.
  • Press Enter to finish creating the folder.

You should see the new weather-search folder inside Documents.

  • Press the Windows key to return to Windows search.
  • Type Visual Studio Code into the search field.
  • Press Enter to open Visual Studio Code.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Select Documents in the folder picker.
  • Select the weather-search folder.

A trust prompt may appear

  • Approve the trust prompt because you created this folder yourself.
  • Click Select Folder to open the workspace.

Good. Visual Studio Code now has a dedicated workspace for every file in Weather Search.

Add the three app files

The three-file split separates page structure from presentation and behavior. This keeps each browser layer visible while you learn.

  • Select the weather-search folder in the Explorer sidebar.
  • Click the new file icon at the top of the Explorer sidebar.
  • Enter index.html as the file name.
  • Press Enter to create the file.

The new index.html file should now appear in the Explorer sidebar.

  • Click the new file icon again.
  • Enter styles.css as the file name.
  • Press Enter to create the file.
  • Click the new file icon once more.
  • Enter script.js as the file name.
  • Press Enter to create the file.

The Explorer sidebar should now list index.html, styles.css, and script.js.

  • Select index.html in the Explorer sidebar.
  • Add the linked page skeleton by pasting this code into index.html:
<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Weather Search</title>
  <link rel="stylesheet" href="styles.css">
  <script src="script.js" defer></script>
</head>
<body>
  <h1>Weather Search Setup</h1>
</body>
</html>

What does this code do?

  • The document structure gives Microsoft Edge a complete HTML page to display.
  • The viewport setting prepares the page to adapt to different screen widths.
  • The styles.css link connects the stylesheet that you build in the next step.
  • The deferred script.js reference loads JavaScript after the page structure is ready.
  • The heading creates a visible checkpoint for this setup.
  • Press Ctrl+S to save index.html.
  • Select the styles.css tab.
  • Press Ctrl+S to save styles.css.
  • Select the script.js tab.
  • Press Ctrl+S to save script.js.
  • Confirm that the Explorer sidebar lists all three files.

Missing a project file?

  • Check that the Explorer sidebar is showing the weather-search folder.
  • Rename any file whose name ends with an extra text extension.
  • Create the missing file with the new file icon in the Explorer sidebar.

Help me check why one of my three project files is missing.

✔️ Awesome, I've got everything!

Great. Your three project files are saved inside the same weather-search folder.

ⓧ I'd like to double check the full code

The complete index.html checkpoint is shown below:

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Weather Search</title>
  <link rel="stylesheet" href="styles.css">
  <script src="script.js" defer></script>
</head>
<body>
  <h1>Weather Search Setup</h1>
</body>
</html>

What should match?

The styles.css and script.js files are both empty at this checkpoint. Their empty state matches the project setup.

Open the page in Microsoft Edge

A browser can open this static HTML file directly from your computer. The linked files do not need a framework or build process.

  • Switch back to Windows File Explorer from earlier.
  • Select Documents in the left navigation.
  • Double-click the weather-search folder.

You should see index.html, styles.css, and script.js inside the folder.

Before you open the page, predict whether Microsoft Edge will display the heading from your HTML file.

  • Launch index.html in Microsoft Edge by double-clicking the file.

You should see the heading Weather Search Setup on the page.

That's your first visible win. Microsoft Edge is reading the HTML file from your three-file app.

Page not opening correctly?

  • Use the Windows app chooser to open index.html with Microsoft Edge if another app opens.
  • Save index.html in Visual Studio Code if the page is blank.
  • Confirm that the file is named index.html without an extra text extension.

Help me debug why index.html does not show Weather Search Setup in Microsoft Edge.

Your three-file app now opens in the browser. Next, you'll turn this setup page into a responsive weather search card.

Build the Static Weather Card

The three-file skeleton now opens in Microsoft Edge. Its setup heading proves that the browser can load your project.

A visible static version gives you an early win. It also defines every page element that JavaScript updates later.

In this step, get ready to:
  • Replace the setup heading with a semantic search interface.
  • Add stable identifiers to the weather display elements.
  • Style the interface for wide and narrow browser windows.
Build the semantic page structure

Semantic HTML gives each part of the page a clear purpose. The browser can distinguish the main content from the search form.

  • Switch back to Visual Studio Code.
  • Select index.html in the Explorer sidebar.
  • Find the heading that contains Weather Search Setup.
  • Replace that heading with the page introduction below:
  <main class="weather-app">
    <section class="panel" aria-labelledby="app-title">
      <p class="eyebrow">Your local forecast</p>
      <h1 id="app-title">Weather Search</h1>
      <p class="intro">Search for a city to see its current conditions.</p>
    </section>
  </main>

What does this markup do?

  • The main element identifies the primary content of the page.
  • The panel class gives the interface one container for its layout and visual styling.
  • aria-labelledby connects the section to the heading with the app-title identifier.
  • Save index.html.
  • Return to Microsoft Edge.
  • Refresh the page.

You should see Your local forecast above the new Weather Search heading. The short search description appears underneath.

Still seeing the setup heading?

Confirm that you edited the index.html file inside the weather-search folder. Save the file before refreshing Microsoft Edge.

Help me find why Microsoft Edge still shows the old heading.

The form needs stable identifiers so JavaScript can find it in the next step. Its status region also provides a dedicated place for loading messages.

  • Insert the search form below the paragraph with the intro class by pasting this markup:
      <form id="search-form" class="search-form">
        <label for="city-input">City</label>
        <div class="search-controls">
          <input id="city-input" name="city" type="search" value="Berlin" placeholder="Try London or Tokyo" autocomplete="off" required>
          <button id="search-button" type="submit">Search</button>
        </div>
      </form>

      <p id="status" class="status" role="status" aria-live="polite">Loading weather for Berlin...</p>

What does this form do?

  • The search-form identifier gives JavaScript a precise target for form submissions.
  • The label links to city-input so the field has an accessible name.
  • The search-button submits the form when the learner clicks it.
  • The status paragraph announces changing messages through its live region.
  • Save index.html.
  • Return to Microsoft Edge.
  • Refresh the page.

You should see a City field containing Berlin. A Search button appears beside it.

You should also see the status message Loading weather for Berlin... below the form.

Form missing or out of place?

Make sure the form sits after the paragraph with the intro class. It must stay inside the section with the panel class.

Help me check the structure of my search form.

Add the weather card and attribution

The weather card holds the values that later come from Open-Meteo. Stable element identifiers let JavaScript replace each placeholder independently.

  • Insert the weather card below the status paragraph by pasting this markup:
      <article id="weather-card" class="weather-card" hidden>
        <header class="weather-card__header">
          <div>
            <p class="eyebrow">Current weather</p>
            <h2 id="location-name"></h2>
          </div>
          <p id="condition" class="condition"></p>
        </header>

        <p class="temperature"><span id="temperature">--</span><span id="temperature-unit">°C</span></p>

        <dl class="details">
          <div class="detail"><dt>Feels like</dt><dd id="apparent-temperature">--</dd></div>
          <div class="detail"><dt>Wind speed</dt><dd id="wind-speed">--</dd></div>
        </dl>
      </article>

      <p class="attribution">Weather data by <a href="https://open-meteo.com/" target="_blank" rel="noreferrer">Open-Meteo</a></p>

How is the card organized?

  • The weather-card article groups the current conditions into one result area.
  • The location-name element holds the matched city and country.
  • The condition element holds a readable weather description.
  • The temperature element is separate from temperature-unit so each value can change independently.
  • The description list pairs the apparent temperature with the wind speed.
  • The attribution link credits the weather data provider.
  • Save index.html.
  • Return to Microsoft Edge.
  • Refresh the page.

You should see the Open-Meteo attribution below the loading message. The weather card starts with its hidden state so a later script can reveal it after weather data arrives.

Attribution not showing?

Check that the attribution paragraph appears before the closing tag for the panel section. A closing section tag placed too early moves the attribution outside the panel.

Help me find a misplaced tag in the weather card markup.

✔️ Awesome, I've got everything!

Great. Double check that you saved index.html before styling the interface.

ⓧ I'd like to double check the full code

Compare your complete index.html file against this version:

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Weather Search</title>
  <link rel="stylesheet" href="styles.css">
  <script src="script.js" defer></script>
</head>
<body>
  <main class="weather-app">
    <section class="panel" aria-labelledby="app-title">
      <p class="eyebrow">Your local forecast</p>
      <h1 id="app-title">Weather Search</h1>
      <p class="intro">Search for a city to see its current conditions.</p>

      <form id="search-form" class="search-form">
        <label for="city-input">City</label>
        <div class="search-controls">
          <input id="city-input" name="city" type="search" value="Berlin" placeholder="Try London or Tokyo" autocomplete="off" required>
          <button id="search-button" type="submit">Search</button>
        </div>
      </form>

      <p id="status" class="status" role="status" aria-live="polite">Loading weather for Berlin...</p>

      <article id="weather-card" class="weather-card" hidden>
        <header class="weather-card__header">
          <div>
            <p class="eyebrow">Current weather</p>
            <h2 id="location-name"></h2>
          </div>
          <p id="condition" class="condition"></p>
        </header>

        <p class="temperature"><span id="temperature">--</span><span id="temperature-unit">°C</span></p>

        <dl class="details">
          <div class="detail"><dt>Feels like</dt><dd id="apparent-temperature">--</dd></div>
          <div class="detail"><dt>Wind speed</dt><dd id="wind-speed">--</dd></div>
        </dl>
      </article>

      <p class="attribution">Weather data by <a href="https://open-meteo.com/" target="_blank" rel="noreferrer">Open-Meteo</a></p>
    </section>
  </main>
</body>
</html>
Style the responsive interface

CSS turns the document structure into a centered weather panel. The first styles establish the page colors and sizing rules.

  • Select styles.css in the Explorer sidebar.
  • Add the page foundation by pasting this code:
:root {
  font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  color: #172033;
  background: #dcecff;
}

* { box-sizing: border-box; }

body {
  min-width: 320px;
  min-height: 100vh;
  margin: 0;
  background: linear-gradient(145deg, #dcecff 0%, #f7fbff 55%, #fff3dd 100%);
}

What do these foundation styles control?

  • The root styles establish the page font and default text color.
  • The universal selector includes borders and padding inside each element's declared size.
  • The body fills the browser height with a three-color gradient.
  • Save styles.css.
  • Return to Microsoft Edge.
  • Refresh the page.

You should see a pale blue gradient behind the interface. The default page margin has disappeared.

Background still plain white?

Confirm that index.html still links to styles.css in its head. Make sure both files are saved in the same folder.

Help me find why styles.css is not loading.

  • Add the centered page layout below the existing CSS by pasting this code:
button, input { font: inherit; }

.weather-app {
  display: grid;
  min-height: 100vh;
  place-items: center;
  padding: 24px;
}

.panel {
  width: min(100%, 620px);
  padding: clamp(24px, 6vw, 48px);
  border: 1px solid rgba(23, 32, 51, 0.1);
  border-radius: 24px;
  background: rgba(255, 255, 255, 0.88);
  box-shadow: 0 24px 70px rgba(30, 73, 118, 0.18);
}

How does the panel stay centered?

  • The weather-app grid centers its child across the full browser height.
  • The panel width stops growing at 620px.
  • The flexible padding grows on larger screens while keeping a compact minimum.
  • The translucent background and shadow separate the panel from the gradient.
  • Save styles.css.
  • Return to Microsoft Edge.
  • Refresh the page.

You should see the content centered inside a rounded white panel. A soft shadow should lift the panel from the background.

Panel not centered?

Check that the class in index.html is spelled weather-app. Confirm that the selector in styles.css begins with a full stop.

Help me debug why the weather panel is not centered.

  • Add the heading and form layout below the panel styles by pasting this code:
h1, h2, p { margin-top: 0; }
h1 { margin-bottom: 8px; font-size: clamp(2rem, 8vw, 3.5rem); line-height: 1; }
h2 { margin-bottom: 0; font-size: 1.4rem; }

.eyebrow {
  margin-bottom: 8px;
  color: #315b86;
  font-size: 0.78rem;
  font-weight: 800;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.intro, .status, .attribution { color: #52627a; }
.search-form { margin-top: 28px; }
.search-form label { display: block; margin-bottom: 8px; font-weight: 700; }
.search-controls { display: grid; grid-template-columns: 1fr auto; gap: 10px; }
input, button { min-height: 48px; border-radius: 12px; }

How does this shape the form?

  • The heading size responds to the available browser width.
  • The eyebrow class creates the small uppercase labels above each heading.
  • The search-controls grid gives the field the flexible column.
  • The input and button share the same minimum height.
  • Save styles.css.
  • Return to Microsoft Edge.
  • Refresh the page.

You should see a larger Weather Search heading. The City field and Search button should share one row.

Form controls stacked too early?

Check that search-controls is the class on the div around the input and button. Confirm that its CSS selector uses grid-template-columns.

Help me fix the form layout.

  • Add the form control styles below the existing CSS by pasting this code:
input {
  width: 100%;
  border: 1px solid #a9b9cc;
  padding: 0 14px;
  color: #172033;
  background: #ffffff;
}

button {
  border: 0;
  padding: 0 20px;
  color: #ffffff;
  background: #155eef;
  font-weight: 800;
  cursor: pointer;
}

button:hover { background: #0f4ac2; }
button:focus-visible, input:focus-visible, a:focus-visible { outline: 3px solid #ffbf47; outline-offset: 3px; }
button:disabled { cursor: wait; opacity: 0.6; }
.status { min-height: 24px; margin: 16px 0; }

What changes for interaction?

  • The input gains a visible border and comfortable horizontal padding.
  • The button uses a strong blue background with a darker hover state.
  • The focus rule adds a yellow outline for keyboard navigation.
  • The disabled rule provides a waiting cursor with reduced opacity.
  • Save styles.css.
  • Return to Microsoft Edge.
  • Refresh the page.
  • Press Tab until the City field receives focus.

You should see a yellow outline around the focused field. The Search button should appear blue with white text.

Focus outline not visible?

Use the keyboard to move focus into the form. The focus-visible styles appear during keyboard navigation.

Help me check why the focus style is missing.

The card starts hidden because live data is not connected yet. You can expose it briefly while styling without changing its final starting state.

  • Add the card container styles below the existing CSS by pasting this code:
.weather-card {
  padding: 24px;
  border-radius: 18px;
  color: #ffffff;
  background: #172f55;
}

.weather-card__header { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; }
.weather-card .eyebrow { color: #bcd9ff; }

.condition {
  margin: 0;
  border-radius: 999px;
  padding: 8px 12px;
  color: #172033;
  background: #dcecff;
  font-size: 0.9rem;
  font-weight: 700;
  text-align: center;
}

How does the weather card stand out?

  • The dark background separates the result from the pale page.
  • The header uses a flexible row for the location and condition.
  • The condition uses a rounded badge style for quick scanning.
  • Save styles.css.
  • Select index.html in the Explorer sidebar.
  • Temporarily delete the hidden attribute from the opening weather card tag.
  • Save index.html.
  • Return to Microsoft Edge.
  • Refresh the page.

You should see a dark weather card below the status message. Its temperature placeholder should read --°C.

Weather card still hidden?

Check the opening tag for weather-card in index.html. Remove only the hidden attribute during this visual check.

Help me expose the weather card for styling.

  • Return to styles.css.
  • Add the temperature details and responsive rules below the existing CSS by pasting this code:
.temperature { margin: 28px 0; font-size: clamp(3rem, 16vw, 5rem); font-weight: 800; line-height: 1; }
.details { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; margin: 0; }
.detail { border-top: 1px solid rgba(255, 255, 255, 0.25); padding-top: 14px; }
dt { color: #bcd9ff; font-size: 0.85rem; }
dd { margin: 4px 0 0; font-size: 1.1rem; font-weight: 800; }
.attribution { margin: 18px 0 0; font-size: 0.82rem; text-align: center; }
.attribution a { color: #155eef; }
[hidden] { display: none !important; }

@media (max-width: 540px) {
  .search-controls, .details { grid-template-columns: 1fr; }
  .weather-card__header { align-items: stretch; flex-direction: column; }
  .condition { align-self: flex-start; }
}

How does the layout respond?

  • The temperature scales with the browser width while keeping readable limits.
  • The details use two equal columns on wider screens.
  • The media query changes the form and details to one column below 540px.
  • The card header changes to a vertical layout at the same width.
  • Save styles.css.
  • Return to Microsoft Edge.
  • Refresh the page.

You should see a large temperature placeholder. The Feels like and Wind speed details should appear in two columns.

Details missing their layout?

Confirm that the description list uses the details class. Check that each child wrapper uses the detail class.

Help me debug the weather detail layout.

The visual check is complete. Restore the card's saved starting state so JavaScript can control when it appears in later steps.

  • Select index.html in the Explorer sidebar.
  • Restore the weather card opening tag so it matches this line:
      <article id="weather-card" class="weather-card" hidden>

Why restore the hidden state?

The hidden attribute prevents placeholder data from appearing during a real weather request. JavaScript reveals the card after it has values to display.

  • Save index.html.
  • Keep the current Microsoft Edge page open without refreshing it.

✔️ Awesome, I've got everything!

Great. Double check that you saved styles.css before testing the narrow layout.

ⓧ I'd like to double check the full code

Compare your complete styles.css file against this version:

:root {
  font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  color: #172033;
  background: #dcecff;
}

* { box-sizing: border-box; }

body {
  min-width: 320px;
  min-height: 100vh;
  margin: 0;
  background: linear-gradient(145deg, #dcecff 0%, #f7fbff 55%, #fff3dd 100%);
}

button, input { font: inherit; }

.weather-app {
  display: grid;
  min-height: 100vh;
  place-items: center;
  padding: 24px;
}

.panel {
  width: min(100%, 620px);
  padding: clamp(24px, 6vw, 48px);
  border: 1px solid rgba(23, 32, 51, 0.1);
  border-radius: 24px;
  background: rgba(255, 255, 255, 0.88);
  box-shadow: 0 24px 70px rgba(30, 73, 118, 0.18);
}

h1, h2, p { margin-top: 0; }
h1 { margin-bottom: 8px; font-size: clamp(2rem, 8vw, 3.5rem); line-height: 1; }
h2 { margin-bottom: 0; font-size: 1.4rem; }

.eyebrow {
  margin-bottom: 8px;
  color: #315b86;
  font-size: 0.78rem;
  font-weight: 800;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.intro, .status, .attribution { color: #52627a; }
.search-form { margin-top: 28px; }
.search-form label { display: block; margin-bottom: 8px; font-weight: 700; }
.search-controls { display: grid; grid-template-columns: 1fr auto; gap: 10px; }
input, button { min-height: 48px; border-radius: 12px; }

input {
  width: 100%;
  border: 1px solid #a9b9cc;
  padding: 0 14px;
  color: #172033;
  background: #ffffff;
}

button {
  border: 0;
  padding: 0 20px;
  color: #ffffff;
  background: #155eef;
  font-weight: 800;
  cursor: pointer;
}

button:hover { background: #0f4ac2; }
button:focus-visible, input:focus-visible, a:focus-visible { outline: 3px solid #ffbf47; outline-offset: 3px; }
button:disabled { cursor: wait; opacity: 0.6; }
.status { min-height: 24px; margin: 16px 0; }

.weather-card {
  padding: 24px;
  border-radius: 18px;
  color: #ffffff;
  background: #172f55;
}

.weather-card__header { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; }
.weather-card .eyebrow { color: #bcd9ff; }

.condition {
  margin: 0;
  border-radius: 999px;
  padding: 8px 12px;
  color: #172033;
  background: #dcecff;
  font-size: 0.9rem;
  font-weight: 700;
  text-align: center;
}

.temperature { margin: 28px 0; font-size: clamp(3rem, 16vw, 5rem); font-weight: 800; line-height: 1; }
.details { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; margin: 0; }
.detail { border-top: 1px solid rgba(255, 255, 255, 0.25); padding-top: 14px; }
dt { color: #bcd9ff; font-size: 0.85rem; }
dd { margin: 4px 0 0; font-size: 1.1rem; font-weight: 800; }
.attribution { margin: 18px 0 0; font-size: 0.82rem; text-align: center; }
.attribution a { color: #155eef; }
[hidden] { display: none !important; }

@media (max-width: 540px) {
  .search-controls, .details { grid-template-columns: 1fr; }
  .weather-card__header { align-items: stretch; flex-direction: column; }
  .condition { align-self: flex-start; }
}

Before you narrow the window, where do you expect the Search button and weather details to move?

  • Return to the Microsoft Edge page that still shows the exposed weather card.
  • Drag the right edge inward until the window is narrower than 540px.

You should see the Search button move below the City field. The weather details should stack into one column.

That is your first complete visual win. The page now adapts from a wide panel to a narrow mobile-style layout.

The static interface is ready. Next, you will turn a city name into real coordinates and update the visible location.

Turn a City Name into Coordinates

Your responsive Weather Search page now has a form and a styled card. Each search result has a clear place to appear.

A typed city is still just text. This step sends that text to the Open-Meteo Geocoding API to find a matching place with precise coordinates.

For now, only the displayed location changes. The unchanged weather placeholders make the next missing piece visible.

In this step, get ready to:
  • Connect the existing form and location element to script.js.
  • Retrieve the first English JSON match for a submitted city.
  • Display the matched city and country without reloading the page.
Connect the form to geocoding

JavaScript uses references to the Document Object Model to read the city field and update the location heading. A form submission listener takes control before the browser reloads the page.

Why geocode before weather?

People search with place names such as Paris. Weather requests need latitude and longitude to identify an exact point.

Geocoding bridges those two formats. The matched coordinates prepare the app for its weather request in the next step.

  • Switch back to script.js in Visual Studio Code.
  • Replace the empty contents of script.js with the complete geocoding flow below:
const GEOCODING_URL = "https://geocoding-api.open-meteo.com/v1/search";

const searchForm = document.querySelector("#search-form");
const cityInput = document.querySelector("#city-input");
const locationName = document.querySelector("#location-name");

async function findLocation(city) {
  const url = new URL(GEOCODING_URL);
  url.search = new URLSearchParams({ name: city, count: "1", language: "en", format: "json" }).toString();
  const response = await fetch(url);
  const data = await response.json();
  return data.results[0];
}

searchForm.addEventListener("submit", async (event) => {
  event.preventDefault();
  const city = cityInput.value.trim();
  if (!city) return;

  const location = await findLocation(city);
  locationName.textContent = `${location.name}, ${location.country}`;
});

What does this code do?

  • The GEOCODING_URL constant stores the endpoint used for city searches.
  • The three document.querySelector() calls connect the script to the form, city field, and location heading.
  • The findLocation(city) function creates a URL from the endpoint. This gives the request a stable base address.
  • The URLSearchParams record requests one English result in JSON format.
  • The fetch() call sends an HTTP GET request. The response.json() call reads the returned data.
  • The submit listener calls event.preventDefault() so the page stays in place.
  • The listener trims the submitted city. It displays the returned name and country inside locationName.

The result also contains latitude and longitude. Those values stay available on the returned location object.

The Open-Meteo attribution link from the previous step remains visible below the card. It credits the data source now that the app makes live requests.

  • Save script.js by pressing Cmd+S (macOS) or Ctrl+S (Windows).

✔️ Awesome, I've got everything!

Great. Double check that you saved script.js before testing the city search.

ⓧ I'd like to double check the full code

Compare your complete script.js file with this reference:

const GEOCODING_URL = "https://geocoding-api.open-meteo.com/v1/search";

const searchForm = document.querySelector("#search-form");
const cityInput = document.querySelector("#city-input");
const locationName = document.querySelector("#location-name");

async function findLocation(city) {
  const url = new URL(GEOCODING_URL);
  url.search = new URLSearchParams({ name: city, count: "1", language: "en", format: "json" }).toString();
  const response = await fetch(url);
  const data = await response.json();
  return data.results[0];
}

searchForm.addEventListener("submit", async (event) => {
  event.preventDefault();
  const city = cityInput.value.trim();
  if (!city) return;

  const location = await findLocation(city);
  locationName.textContent = `${location.name}, ${location.country}`;
});

What should match?

Your file should contain one endpoint constant and three page references. It should also contain the findLocation(city) function and one form submission listener.

Search for Paris and verify the result

A successful search proves that the browser can send a city name to Open-Meteo and use the returned match. The weather values stay unchanged because this step updates only the location.

  • Switch back to the Weather Search page in Microsoft Edge.
  • Refresh index.html.
  • Replace the value in the City field with Paris.

Before you submit the form, do you expect the page to reload or the location text to change in place?

  • Press Enter while the city field is focused.

You'll see Paris, France in the weather card. The temperature still shows -- with °C.

That first live search is working. A city name now becomes a matched place without a page reload.

Location not changing?

  • Confirm that script.js is saved inside the weather-search folder.
  • Compare #search-form, #city-input, and #location-name with the identifiers in index.html.
  • Check that your internet connection allows the browser request to complete.

Help me debug why my city search does not update the location.

Your form now turns a city name into a precise location result. Next, you'll use those coordinates to replace every placeholder with live weather data.

Replace the Placeholders with Live Weather

A Paris search now resolves the city name to precise coordinates. The weather card still shows placeholder conditions.

Those coordinates give the Open-Meteo Forecast API the exact point needed for a forecast. This step builds the successful data path. An unknown city then reveals the missing recovery path.

In this step, get ready to:
  • Request current weather for geocoded coordinates.
  • Render the returned conditions inside the weather card.
  • Test automatic Berlin loading against an invalid-city search.
Request current conditions

The geocoding request finds a place. A second request uses that place's latitude and longitude to retrieve its current conditions.

  • In script.js, find this opening block:
const GEOCODING_URL = "https://geocoding-api.open-meteo.com/v1/search";

const searchForm = document.querySelector("#search-form");
const cityInput = document.querySelector("#city-input");
const locationName = document.querySelector("#location-name");

What Are You Expanding?

This block currently stores the geocoding endpoint. It also selects the form elements used by the earlier search flow.

  • Replace that opening block with this expanded setup:
const GEOCODING_URL = "https://geocoding-api.open-meteo.com/v1/search";
const FORECAST_URL = "https://api.open-meteo.com/v1/forecast";

const searchForm = document.querySelector("#search-form");
const cityInput = document.querySelector("#city-input");
const searchButton = document.querySelector("#search-button");
const statusMessage = document.querySelector("#status");
const weatherCard = document.querySelector("#weather-card");
const locationName = document.querySelector("#location-name");
const condition = document.querySelector("#condition");
const temperature = document.querySelector("#temperature");
const temperatureUnit = document.querySelector("#temperature-unit");
const apparentTemperature = document.querySelector("#apparent-temperature");
const windSpeed = document.querySelector("#wind-speed");

const weatherCodes = { 0: "Clear sky", 1: "Mainly clear", 2: "Partly cloudy", 3: "Overcast" };

function getWeatherDescription(code) {
  return weatherCodes[code] ?? "Unknown conditions";
}

What Does This Setup Add?

  • The FORECAST_URL constant stores the forecast endpoint.
  • The new element references connect JavaScript to every value in the weather card.
  • The weatherCodes object translates the first four weather codes into readable conditions.
  • The getWeatherDescription(code) function supplies a fallback for every other code.
  • Add this function below findLocation(city) in script.js to request the current conditions:
async function fetchCurrentWeather(latitude, longitude) {
  const url = new URL(FORECAST_URL);
  url.search = new URLSearchParams({ latitude: String(latitude), longitude: String(longitude), current: "temperature_2m,apparent_temperature,weather_code,wind_speed_10m", temperature_unit: "celsius", wind_speed_unit: "kmh" }).toString();
  const response = await fetch(url);
  return response.json();
}

What Does This Function Do?

  • The URL object starts with the forecast endpoint.
  • The URLSearchParams object adds the coordinates to the request.
  • The current parameter asks for temperature data plus the weather code and wind speed.
  • The response becomes JSON that JavaScript can read.
  • Save script.js by pressing Ctrl+S.
  • Return to Microsoft Edge.
  • Enter Paris in the city field.
  • Click Search.

The location changes to Paris, France. The placeholder weather values remain. This confirms the earlier geocoding flow still works.

Paris No Longer Appearing?

Check that every selector begins with #. Confirm that the closing brace for getWeatherDescription(code) appears before findLocation(city).

Compare the forecast function with the code above if the browser stops updating.

Help me find the mistake in my weather request setup.

Render the returned weather

The forecast response contains values under current. Its display units live under current_units.

  • Add this function below fetchCurrentWeather(latitude, longitude) in script.js to update the card:
function renderWeather(location, weather) {
  const current = weather.current;
  const units = weather.current_units;
  locationName.textContent = `${location.name}, ${location.country}`;
  condition.textContent = getWeatherDescription(current.weather_code);
  temperature.textContent = Math.round(current.temperature_2m);
  temperatureUnit.textContent = units.temperature_2m;
  apparentTemperature.textContent = `${Math.round(current.apparent_temperature)} ${units.apparent_temperature}`;
  windSpeed.textContent = `${Math.round(current.wind_speed_10m)} ${units.wind_speed_10m}`;
  weatherCard.hidden = false;
}

How Does Rendering Work?

  • The current constant gives the function shorter access to the live readings.
  • The units constant preserves the labels returned by the API.
  • The temperature readings are rounded before they reach the page.
  • Setting weatherCard.hidden to false reveals the completed card.
  • Inside the existing submission listener, find this location update line:
  locationName.textContent = `${location.name}, ${location.country}`;

What Does the Current Line Do?

This line only changes the location heading. The weather placeholders remain untouched.

  • Replace that line with these forecast and rendering calls:
  const weather = await fetchCurrentWeather(location.latitude, location.longitude);
  renderWeather(location, weather);

How Do the Requests Connect?

The first new line passes the matched coordinates into fetchCurrentWeather(latitude, longitude). The second line sends both API results to renderWeather(location, weather).

  • Save script.js by pressing Ctrl+S.
  • Return to Microsoft Edge.
  • Enter Paris in the city field.
  • Click Search.

You should see Paris, France in the card. You should also see a live temperature plus the returned unit. The apparent temperature and wind speed now replace their placeholders.

Weather Card Still Hidden?

Confirm that renderWeather(location, weather) runs after the forecast request. Check that weatherCard.hidden = false remains inside the rendering function.

A misspelled element identifier can also stop the function before the card becomes visible.

Help me debug why my live weather card stays hidden.

Load Berlin and test the boundary

The same request sequence now belongs in one reusable function. That function can support form submissions plus the first Berlin forecast shown when the page opens.

  • Add this function below renderWeather(location, weather) in script.js to coordinate the loading state:
async function loadWeather(city) {
  statusMessage.textContent = `Loading weather for ${city}...`;
  searchButton.disabled = true;
  weatherCard.hidden = true;
  const location = await findLocation(city);
  const weather = await fetchCurrentWeather(location.latitude, location.longitude);
  renderWeather(location, weather);
  statusMessage.textContent = `Showing current weather for ${location.name}.`;
  searchButton.disabled = false;
}

What Does the Loading Function Control?

  • The status message tells you which city is loading.
  • The disabled Search button prevents duplicate submissions during the requests.
  • The hidden card keeps stale readings out of view.
  • The final lines render the successful response before restoring the Search button.
  • Save script.js by pressing Ctrl+S.
  • Return to Microsoft Edge.
  • Submit another Paris search.

Paris should still load with live conditions. This confirms that the new function has valid syntax before it takes over the form.

Paris Stopped Loading?

Check the braces around loadWeather(city). Confirm that the function sits outside renderWeather(location, weather).

Help me check the structure of my loadWeather function.

  • Select the entire existing searchForm.addEventListener block at the bottom of script.js.
  • Replace it with this submission listener plus the initial Berlin load:
searchForm.addEventListener("submit", (event) => {
  event.preventDefault();
  const city = cityInput.value.trim();
  if (city) loadWeather(city);
});

loadWeather(cityInput.value);

How Does the App Start?

  • The submission listener now sends every non-empty city to loadWeather(city).
  • The final call reads the initial Berlin value from the city field when the page opens.
  • Save script.js by pressing Ctrl+S.

✔️ Awesome, I've got everything!

Great. Double check that you saved script.js before testing the finished success path.

ⓧ I'd like to double check the full code

Compare your complete script.js file with this version:

const GEOCODING_URL = "https://geocoding-api.open-meteo.com/v1/search";
const FORECAST_URL = "https://api.open-meteo.com/v1/forecast";

const searchForm = document.querySelector("#search-form");
const cityInput = document.querySelector("#city-input");
const searchButton = document.querySelector("#search-button");
const statusMessage = document.querySelector("#status");
const weatherCard = document.querySelector("#weather-card");
const locationName = document.querySelector("#location-name");
const condition = document.querySelector("#condition");
const temperature = document.querySelector("#temperature");
const temperatureUnit = document.querySelector("#temperature-unit");
const apparentTemperature = document.querySelector("#apparent-temperature");
const windSpeed = document.querySelector("#wind-speed");

const weatherCodes = { 0: "Clear sky", 1: "Mainly clear", 2: "Partly cloudy", 3: "Overcast" };

function getWeatherDescription(code) {
  return weatherCodes[code] ?? "Unknown conditions";
}

async function findLocation(city) {
  const url = new URL(GEOCODING_URL);
  url.search = new URLSearchParams({ name: city, count: "1", language: "en", format: "json" }).toString();
  const response = await fetch(url);
  const data = await response.json();
  return data.results[0];
}

async function fetchCurrentWeather(latitude, longitude) {
  const url = new URL(FORECAST_URL);
  url.search = new URLSearchParams({ latitude: String(latitude), longitude: String(longitude), current: "temperature_2m,apparent_temperature,weather_code,wind_speed_10m", temperature_unit: "celsius", wind_speed_unit: "kmh" }).toString();
  const response = await fetch(url);
  return response.json();
}

function renderWeather(location, weather) {
  const current = weather.current;
  const units = weather.current_units;
  locationName.textContent = `${location.name}, ${location.country}`;
  condition.textContent = getWeatherDescription(current.weather_code);
  temperature.textContent = Math.round(current.temperature_2m);
  temperatureUnit.textContent = units.temperature_2m;
  apparentTemperature.textContent = `${Math.round(current.apparent_temperature)} ${units.apparent_temperature}`;
  windSpeed.textContent = `${Math.round(current.wind_speed_10m)} ${units.wind_speed_10m}`;
  weatherCard.hidden = false;
}

async function loadWeather(city) {
  statusMessage.textContent = `Loading weather for ${city}...`;
  searchButton.disabled = true;
  weatherCard.hidden = true;
  const location = await findLocation(city);
  const weather = await fetchCurrentWeather(location.latitude, location.longitude);
  renderWeather(location, weather);
  statusMessage.textContent = `Showing current weather for ${location.name}.`;
  searchButton.disabled = false;
}

searchForm.addEventListener("submit", (event) => {
  event.preventDefault();
  const city = cityInput.value.trim();
  if (city) loadWeather(city);
});

loadWeather(cityInput.value);

Before you refresh, do you think Berlin will appear automatically without pressing Search?

  • Refresh index.html in Microsoft Edge.

You should first see the loading status. Berlin then appears with live temperature, apparent temperature, a condition label, and wind speed.

Berlin Weather Not Appearing?

Confirm that loadWeather(cityInput.value) is the final line in script.js. Check that the city field still starts with Berlin.

Make sure Microsoft Edge has internet access because both requests need the public APIs.

Help me debug why Berlin does not load when the page opens.

Before you submit an unknown city, do you think this success-only version can restore the interface when no location is returned?

  • Replace the city field value with zzzzzzzzzzzzzz.
  • Press Enter to submit the form from the keyboard.

The interface stays on the loading message. The Search button remains disabled. The weather card stays hidden.

This Shortfall Is Intentional

The location request returns no matching object. The next operation cannot read coordinates from that missing result.

The current loadWeather(city) function has no recovery path. This test exposes the exact reliability gap that remains.

Your app now turns valid city searches into live weather cards. Next, you'll add loading recovery plus useful messages for unknown cities and failed requests.

Make Every Search Feel Reliable

Your app now turns valid city searches into live weather. The next goal is to make every search finish in a clear state.

An unknown city can return no location result. A failed request can also arrive as an HTTP error.

You'll add explicit loading and error states. A guaranteed cleanup path will make the next search usable after every outcome.

In this step, get ready to:
  • Check geocoding and forecast responses before reading their JSON.
  • Handle loading, success, failure, and cleanup inside loadWeather(city).
  • Test button, keyboard, unknown-city, and narrow-screen behavior.
Check every API response

The Fetch API fulfills its promise when a server returns an HTTP error status. Your code must inspect response.ok before parsing the response as JSON.

A successful Open-Meteo geocoding response can also contain no matching locations. That empty result needs its own clear message.

A successful forecast can return many official weather codes. Completing the condition map keeps those valid results readable.

  • Switch back to Visual Studio Code.
  • Select script.js in the Explorer sidebar.
  • Find the existing weatherCodes object.
  • Replace the existing condition map and getWeatherDescription(code) function by pasting this code:
const weatherCodes = {
  0: "Clear sky", 1: "Mainly clear", 2: "Partly cloudy", 3: "Overcast",
  45: "Fog", 48: "Depositing rime fog", 51: "Light drizzle",
  53: "Moderate drizzle", 55: "Dense drizzle", 56: "Light freezing drizzle",
  57: "Dense freezing drizzle", 61: "Slight rain", 63: "Moderate rain",
  65: "Heavy rain", 66: "Light freezing rain", 67: "Heavy freezing rain",
  71: "Slight snow fall", 73: "Moderate snow fall", 75: "Heavy snow fall",
  77: "Snow grains", 80: "Slight rain showers", 81: "Moderate rain showers",
  82: "Violent rain showers", 85: "Slight snow showers", 86: "Heavy snow showers",
  95: "Thunderstorm", 96: "Thunderstorm with slight hail",
  99: "Thunderstorm with heavy hail"
};

function getWeatherDescription(code) {
  return weatherCodes[code] ?? "Unknown conditions";
}

How does the condition map help?

  • Each numeric key maps a returned weather code to a description that someone can understand.
  • The fallback keeps the interface readable if the API returns a code outside the map.
  • Save script.js with Ctrl+S.
  • Switch back to Microsoft Edge.
  • Refresh index.html.

You'll see Berlin's live weather card with a readable condition such as clear sky, fog, rain, or snow.

Still seeing unknown conditions?

Compare the numeric keys in your weatherCodes object with the code above. Check that getWeatherDescription(code) still reads from the same object.

Help me check my weather-code mapping in script.js.

The geocoding request has two failure shapes. A failed HTTP response has an unsuccessful status while an unknown city has no usable results entry.

  • Find the existing findLocation(city) function in script.js.
  • Replace the complete function by pasting this code:
async function findLocation(city) {
  const url = new URL(GEOCODING_URL);
  url.search = new URLSearchParams({ name: city, count: "1", language: "en", format: "json" }).toString();
  const response = await fetch(url);

  if (!response.ok) {
    throw new Error("The city search request failed. Please try again.");
  }

  const data = await response.json();

  if (!data.results?.length) {
    throw new Error("City not found. Try a more specific search.");
  }

  return data.results[0];
}

What does this code do?

  • The response.ok check turns an unsuccessful HTTP response into a clear city-search error.
  • The data.results?.length check catches missing or empty location results.
  • Each throw new Error(...) statement sends a useful message to the caller.
  • The final return keeps the first matched location for a successful search.
  • Save script.js.
  • Return to Microsoft Edge.
  • Refresh index.html.

You'll see Berlin load with live conditions. This confirms the successful geocoding path still reaches the forecast request.

Does Berlin stop loading?

Check that return data.results[0] remains inside findLocation(city). A missing brace can place that return outside the function.

Help me debug the updated findLocation function.

The forecast request also needs to reject unsuccessful HTTP responses. This prevents an error payload from reaching renderWeather(location, weather) as if it contained current conditions.

  • Find the existing fetchCurrentWeather(latitude, longitude) function in script.js.
  • Replace the complete function by pasting this code:
async function fetchCurrentWeather(latitude, longitude) {
  const url = new URL(FORECAST_URL);
  url.search = new URLSearchParams({
    latitude: String(latitude),
    longitude: String(longitude),
    current: "temperature_2m,apparent_temperature,weather_code,wind_speed_10m",
    temperature_unit: "celsius",
    wind_speed_unit: "kmh"
  }).toString();

  const response = await fetch(url);

  if (!response.ok) {
    throw new Error("The weather request failed. Please try again.");
  }

  return response.json();
}

How does the forecast check work?

  • The request still sends the matched coordinates and asks for the four current-weather values.
  • The response.ok check stops an unsuccessful response before its body reaches the renderer.
  • A successful response continues through response.json().
  • Save script.js.
  • Switch back to Microsoft Edge.
  • Refresh index.html.
  • Replace the city field value with London.
  • Click Search.

You'll see London's matched location with live temperature, apparent temperature, condition, and wind speed.

Does the forecast fail?

Check that the response.ok block appears after fetch(url). Confirm that return response.json() remains after the check.

Help me debug the updated forecast request.

Recover from every search outcome

The request functions now throw useful errors. The loadWeather(city) function needs to catch those errors without leaving stale weather or a disabled button behind.

A try block handles the successful path. The matching catch and finally blocks display failures and restore the controls.

  • Find the existing loadWeather(city) function in script.js.
  • Replace the complete function by pasting this code:
async function loadWeather(city) {
  statusMessage.textContent = `Loading weather for ${city}...`;
  searchButton.disabled = true;
  weatherCard.hidden = true;

  try {
    const location = await findLocation(city);
    const weather = await fetchCurrentWeather(location.latitude, location.longitude);
    renderWeather(location, weather);
    statusMessage.textContent = `Showing current weather for ${location.name}.`;
  } catch (error) {
    statusMessage.textContent = error instanceof Error ? error.message : "Something went wrong. Please try again.";
  } finally {
    searchButton.disabled = false;
  }
}

What does this code do?

  • The opening lines show the requested city in the loading message.
  • They also disable searchButton and hide stale weather while the requests run.
  • The try block renders the matched location and current weather after both requests succeed.
  • The catch block places the thrown message in the live status region.
  • The finally block re-enables the button after success or failure.
  • Save script.js.
  • Switch back to Microsoft Edge.
  • Replace the city field value with zzzzzzzzzzzzzz.

Before you submit this search, do you expect the interface to remain stuck or recover for another search?

  • Click Search.

You'll see City not found. Try a more specific search. in the status region. The weather card stays hidden.

That is the failure path repaired. The Search button is available again because finally restored it.

Is Search still disabled?

Check that searchButton.disabled = false appears inside the finally block. Confirm that the block closes before the function's final brace.

Help me debug why the Search button stays disabled.

✔️ Awesome, I've got everything!

Great. Double check that you saved script.js before testing the complete app.

ⓧ I'd like to double check the full code

Compare your complete script.js file with this version:

const GEOCODING_URL = "https://geocoding-api.open-meteo.com/v1/search";
const FORECAST_URL = "https://api.open-meteo.com/v1/forecast";

const searchForm = document.querySelector("#search-form");
const cityInput = document.querySelector("#city-input");
const searchButton = document.querySelector("#search-button");
const statusMessage = document.querySelector("#status");
const weatherCard = document.querySelector("#weather-card");
const locationName = document.querySelector("#location-name");
const condition = document.querySelector("#condition");
const temperature = document.querySelector("#temperature");
const temperatureUnit = document.querySelector("#temperature-unit");
const apparentTemperature = document.querySelector("#apparent-temperature");
const windSpeed = document.querySelector("#wind-speed");

const weatherCodes = {
  0: "Clear sky", 1: "Mainly clear", 2: "Partly cloudy", 3: "Overcast",
  45: "Fog", 48: "Depositing rime fog", 51: "Light drizzle",
  53: "Moderate drizzle", 55: "Dense drizzle", 56: "Light freezing drizzle",
  57: "Dense freezing drizzle", 61: "Slight rain", 63: "Moderate rain",
  65: "Heavy rain", 66: "Light freezing rain", 67: "Heavy freezing rain",
  71: "Slight snow fall", 73: "Moderate snow fall", 75: "Heavy snow fall",
  77: "Snow grains", 80: "Slight rain showers", 81: "Moderate rain showers",
  82: "Violent rain showers", 85: "Slight snow showers", 86: "Heavy snow showers",
  95: "Thunderstorm", 96: "Thunderstorm with slight hail",
  99: "Thunderstorm with heavy hail"
};

function getWeatherDescription(code) {
  return weatherCodes[code] ?? "Unknown conditions";
}

async function findLocation(city) {
  const url = new URL(GEOCODING_URL);
  url.search = new URLSearchParams({ name: city, count: "1", language: "en", format: "json" }).toString();
  const response = await fetch(url);

  if (!response.ok) {
    throw new Error("The city search request failed. Please try again.");
  }

  const data = await response.json();

  if (!data.results?.length) {
    throw new Error("City not found. Try a more specific search.");
  }

  return data.results[0];
}

async function fetchCurrentWeather(latitude, longitude) {
  const url = new URL(FORECAST_URL);
  url.search = new URLSearchParams({
    latitude: String(latitude),
    longitude: String(longitude),
    current: "temperature_2m,apparent_temperature,weather_code,wind_speed_10m",
    temperature_unit: "celsius",
    wind_speed_unit: "kmh"
  }).toString();

  const response = await fetch(url);

  if (!response.ok) {
    throw new Error("The weather request failed. Please try again.");
  }

  return response.json();
}

function renderWeather(location, weather) {
  const current = weather.current;
  const units = weather.current_units;

  locationName.textContent = `${location.name}, ${location.country}`;
  condition.textContent = getWeatherDescription(current.weather_code);
  temperature.textContent = Math.round(current.temperature_2m);
  temperatureUnit.textContent = units.temperature_2m;
  apparentTemperature.textContent = `${Math.round(current.apparent_temperature)} ${units.apparent_temperature}`;
  windSpeed.textContent = `${Math.round(current.wind_speed_10m)} ${units.wind_speed_10m}`;
  weatherCard.hidden = false;
}

async function loadWeather(city) {
  statusMessage.textContent = `Loading weather for ${city}...`;
  searchButton.disabled = true;
  weatherCard.hidden = true;

  try {
    const location = await findLocation(city);
    const weather = await fetchCurrentWeather(location.latitude, location.longitude);
    renderWeather(location, weather);
    statusMessage.textContent = `Showing current weather for ${location.name}.`;
  } catch (error) {
    statusMessage.textContent = error instanceof Error ? error.message : "Something went wrong. Please try again.";
  } finally {
    searchButton.disabled = false;
  }
}

searchForm.addEventListener("submit", (event) => {
  event.preventDefault();
  const city = cityInput.value.trim();
  if (city) loadWeather(city);
});

loadWeather(cityInput.value);
Test the complete search flow

Each individual fix works. The final check proves that loading, success, failure, keyboard submission, and responsive layout work together.

  • Switch back to Microsoft Edge.
  • Refresh index.html.

You'll see Berlin load automatically. The status changes to Showing current weather for Berlin. after the card receives live conditions.

  • Replace the city field value with Paris.
  • Click Search.

You'll see Paris, France with live weather values. The page stays in place throughout the search.

  • Replace the city field value with zzzzzzzzzzzzzz.

Before you click Search, what should replace the previous weather card when no city matches?

  • Click Search.

You'll see City not found. Try a more specific search. while the stale Paris card remains hidden. The Search button becomes available again.

  • Replace the city field value with London.

Before you submit from the keyboard, do you expect the app to recover from the previous error?

  • Press Enter while the city field is active.

You'll see London's live weather card replace the error state. This proves keyboard submission uses the same recovery path as the button.

  • Drag the right edge of Microsoft Edge inward until the page is narrower than 540px.

You'll see the city field above the Search button. The two weather details stack into one column.

You have closed the full search loop. The app now moves from loading to success or error without trapping the next search.

Secret mission

Add a Celsius and Fahrenheit Toggle

Your weather card currently presents every forecast in Celsius. Add a unit control that preserves the active city while requesting fresh conditions in Celsius or Fahrenheit.

Clean Up Your Resources

Clean Up Your Resources

Choose whether to keep the local app ready, pause it for later, or delete it entirely. The project creates no paid resources, so there are no ongoing costs.

Resources you used:

  • Local weather-search folder in Windows Documents containing index.html, styles.css, and script.js.

Keep everything running

No action is needed. Choose this option if you want to demonstrate the app again or continue improving it.

  • Keep the weather-search folder in Windows Documents.
  • Launch the app again by double-clicking index.html inside the folder.
  • The app requests fresh weather data only while you use it in the browser.

Pause - I'll come back to this later

Pausing means ending the browser session while preserving every project file. The app has no local server process to stop.

  • Close the index.html tab in Microsoft Edge.
  • Keep the weather-search folder in Windows Documents for your next session.
  • Return to the project later by opening index.html from the same folder.

Delete - I don't want to use this again

Remove the local project if you want to start fresh. Deleting the folder affects only the files inside weather-search.

  • Close the index.html tab in Microsoft Edge.
  • Close Visual Studio Code so the project folder is no longer open.
  • Press the Windows key to open search.
  • Type File Explorer into the search box.
  • Press Enter to open Windows File Explorer.
  • Select Documents in the left sidebar.
  • Right-click the weather-search folder.
  • Choose the delete option from the context menu.
  • Confirm that weather-search no longer appears in Documents.

Once the folder disappears, that is the cleanup complete. Your local app files are removed.

Still see the folder?

  • Close any file from weather-search that remains open.
  • Refresh the Documents view in Windows File Explorer.
  • Repeat the delete action on weather-search.

Need a hand? Help me remove the local weather-search folder.

Nice Work!

Nice Work!

You did it! Weather Search now runs as a responsive browser app in Microsoft Edge. Your JavaScript turns city searches into live current conditions.

What you learned:

  • Built a responsive interface with semantic HTML. Used CSS layouts to keep the weather card usable in a narrow window.
  • Connected the Open-Meteo Geocoding API to the Open-Meteo Forecast API. Used asynchronous JavaScript to turn each city into live JSON weather data.
  • Updated the DOM with each API response. Added clear loading states. Built error recovery that re-enables Search after failed requests.
  • Secret Mission: added reusable request state for a Celsius-or-Fahrenheit toggle. The control reloads the active city with the selected unit.

Ready to quiz yourself?