Building a Real-Time Weather Dashboard with the OpenWeather API

Beginner 8 views Jul 13, 2026

A weather dashboard is one of the best beginner-to-intermediate projects for practicing API integration in vanilla JavaScript. In this tutorial you'll build a fully functional dashboard that fetches live conditions for any city in the world, using nothing but HTML, CSS, and JavaScript.

This project is well suited to beginners learning API integration, students practicing asynchronous JavaScript, and developers who want a practical, real-world app with live data rather than another static layout exercise.

By the end of this guide you'll have a working dashboard that shows current conditions, temperature, humidity, wind speed, cloud cover, visibility, and more, all updated in real time.

What You'll Learn

  • How to work with REST APIs and fetch live data using JavaScript
  • How to handle asynchronous operations with async/await
  • How to parse and display JSON data dynamically
  • How to build a responsive weather UI with dynamic content
  • How to handle API errors gracefully
  • How to implement search functionality for any city
  • How to keep API keys out of your committed code

Features of This Weather Dashboard

  • Real-time weather data from the OpenWeather API
  • Search for any city worldwide
  • Displays temperature, humidity, wind speed, and more
  • Dynamic weather icons based on current conditions
  • Responsive design that works on all devices
  • Error handling for invalid city names
  • Clean, modern interface
  • Keyboard-accessible search

Prerequisites

Before starting, you should be comfortable with the basics of HTML, CSS, and JavaScript. You'll also need a code editor such as Visual Studio Code, a modern web browser, and a free API key from a weather service.

Where to Find a Free Weather API

You'll need a free API key to fetch weather data. Here are the best options and what their free tiers offer:

1. OpenWeatherMap (used in this tutorial)

Website: openweathermap.org/api
Free tier: 1,000 calls/day, 60 calls/minute
Features: Current weather, 5-day forecast, UV index, air pollution data
How to get it: Sign up for a free account; your API key becomes available in your account dashboard.

2. WeatherAPI.com

Website: weatherapi.com
Free tier: 1,000,000 calls/month
Features: Current weather, forecast, astronomy data, air quality

3. Visual Crossing Weather

Website: visualcrossing.com
Free tier: 1,000 records/day
Features: Historical weather, forecasts, current conditions

4. Weatherstack API

Website: weatherstack.com
Free tier: 100 requests/month
Features: Current weather, historical data, forecasts

5. Open-Meteo (no API key required)

Website: open-meteo.com
Free tier: Completely free, no signup needed
Features: Current weather, forecasts, historical data

Recommendation: This tutorial uses OpenWeatherMap as the primary source since it's the most widely documented option. The code also includes an Open-Meteo fallback, so the dashboard still works even without an API key.

Project Structure

TEXT
weather-dashboard/
├── index.html
├── style.css
├── script.js
└── .env (optional - for API key security)

Step 1: Build the HTML Structure

Create an index.html file with the following markup:

HTML
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Weather Dashboard - Real-Time Weather App</title>
    <meta name="description" content="Real-time weather dashboard with live data from OpenWeather API.">
    <link rel="stylesheet" href="style.css">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
</head>
<body>
    <div class="container">
        <header>
            <h1>🌤️ Weather Dashboard</h1>
            <p class="subtitle">Real-time weather for any location</p>
        </header>

        <!-- ===== SEARCH SECTION ===== -->
        <div class="search-section">
            <div class="search-box">
                <input
                    type="text"
                    id="cityInput"
                    placeholder="Enter city name..."
                    autocomplete="off"
                    aria-label="Search for a city"
                >
                <button id="searchBtn" aria-label="Search weather">
                    <i class="fas fa-search"></i>
                </button>
            </div>
            <button id="locationBtn" class="location-btn" aria-label="Get weather for your location">
                <i class="fas fa-location-dot"></i>
                Use My Location
            </button>
        </div>

        <!-- ===== LOADING STATE ===== -->
        <div class="loading" id="loading">
            <div class="spinner"></div>
            <p>Fetching weather data...</p>
        </div>

        <!-- ===== WEATHER DISPLAY ===== -->
        <div class="weather-display" id="weatherDisplay" style="display: none;">
            <div class="weather-header">
                <div>
                    <h2 id="cityName">City Name</h2>
                    <p id="countryName">Country</p>
                </div>
                <div class="weather-icon">
                    <img id="weatherIcon" src="" alt="Weather condition">
                    <p id="weatherDescription">Clear Sky</p>
                </div>
            </div>

            <div class="temperature-section">
                <h1 id="temperature">--°C</h1>
                <p id="feelsLike">Feels like --°C</p>
            </div>

            <div class="weather-details">
                <div class="detail-card">
                    <i class="fas fa-water"></i>
                    <div>
                        <span id="humidity">--%</span>
                        <p>Humidity</p>
                    </div>
                </div>
                <div class="detail-card">
                    <i class="fas fa-wind"></i>
                    <div>
                        <span id="windSpeed">-- km/h</span>
                        <p>Wind Speed</p>
                    </div>
                </div>
                <div class="detail-card">
                    <i class="fas fa-cloud"></i>
                    <div>
                        <span id="cloudCover">--%</span>
                        <p>Cloud Cover</p>
                    </div>
                </div>
                <div class="detail-card">
                    <i class="fas fa-eye"></i>
                    <div>
                        <span id="visibility">-- km</span>
                        <p>Visibility</p>
                    </div>
                </div>
            </div>

            <div class="additional-info">
                <div>
                    <i class="fas fa-sun"></i>
                    <span>UV Index: <strong id="uvIndex">--</strong></span>
                </div>
                <div>
                    <i class="fas fa-arrow-up"></i>
                    <span>Pressure: <strong id="pressure">-- hPa</strong></span>
                </div>
            </div>
        </div>

        <!-- ===== ERROR MESSAGE ===== -->
        <div class="error-message" id="errorMessage" style="display: none;">
            <i class="fas fa-exclamation-circle"></i>
            <p id="errorText">City not found. Please try again.</p>
        </div>

        <!-- ===== FOOTER ===== -->
        <footer>
            <p>Data provided by <a href="#" id="apiSource">OpenWeatherMap</a></p>
        </footer>
    </div>

    <script src="script.js"></script>
</body>
</html>

Step 2: Add the CSS Styling

Create a style.css file and add the following:

CSS
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700;800&display=swap');

/* ===== RESET & VARIABLES ===== */
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

:root {
    --primary: #4A90D9;
    --primary-dark: #2C5F8A;
    --secondary: #6C63FF;
    --success: #4CAF50;
    --danger: #FF6B6B;
    --warning: #FFA94D;
    --text: #2D3436;
    --text-light: #636E72;
    --white: #FFFFFF;
    --shadow: 0 10px 30px rgba(0,0,0,0.15);
    --radius: 16px;
    --transition: all 0.3s ease;
}

body {
    font-family: 'Poppins', sans-serif;
    min-height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
    padding: 20px;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    background-attachment: fixed;
}

/* ===== CONTAINER ===== */
.container {
    max-width: 700px;
    width: 100%;
    background: rgba(255,255,255,0.95);
    backdrop-filter: blur(10px);
    border-radius: var(--radius);
    padding: 40px;
    box-shadow: var(--shadow);
    animation: slideUp 0.6s ease;
}

@keyframes slideUp {
    from { opacity: 0; transform: translateY(40px); }
    to { opacity: 1; transform: translateY(0); }
}

/* ===== HEADER ===== */
header {
    text-align: center;
    margin-bottom: 25px;
}

header h1 {
    font-size: 2.2rem;
    font-weight: 700;
    color: var(--text);
    letter-spacing: -0.5px;
}

header .subtitle {
    color: var(--text-light);
    font-size: 0.95rem;
    margin-top: 4px;
}

/* ===== SEARCH ===== */
.search-section {
    display: flex;
    gap: 12px;
    flex-wrap: wrap;
    margin-bottom: 25px;
}

.search-box {
    flex: 1;
    display: flex;
    background: #F0F0F0;
    border-radius: 50px;
    overflow: hidden;
    transition: var(--transition);
    min-width: 200px;
}

.search-box:focus-within {
    box-shadow: 0 0 0 3px rgba(74, 144, 217, 0.2);
}

#cityInput {
    flex: 1;
    padding: 14px 20px;
    border: none;
    background: transparent;
    font-size: 1rem;
    font-family: 'Poppins', sans-serif;
    color: var(--text);
    outline: none;
}

#cityInput::placeholder {
    color: var(--text-light);
}

#searchBtn {
    padding: 14px 22px;
    background: var(--primary);
    color: var(--white);
    border: none;
    cursor: pointer;
    font-size: 1rem;
    transition: var(--transition);
}

#searchBtn:hover {
    background: var(--primary-dark);
}

.location-btn {
    padding: 14px 20px;
    background: var(--secondary);
    color: var(--white);
    border: none;
    border-radius: 50px;
    font-size: 0.9rem;
    font-weight: 500;
    font-family: 'Poppins', sans-serif;
    cursor: pointer;
    transition: var(--transition);
    display: flex;
    align-items: center;
    gap: 8px;
    white-space: nowrap;
}

.location-btn:hover {
    background: #5A52D5;
    transform: scale(1.02);
}

.location-btn:active {
    transform: scale(0.98);
}

/* ===== LOADING ===== */
.loading {
    text-align: center;
    padding: 40px 20px;
    display: none;
}

.loading.visible {
    display: block;
}

.spinner {
    width: 50px;
    height: 50px;
    border: 4px solid #F0F0F0;
    border-top: 4px solid var(--primary);
    border-radius: 50%;
    animation: spin 0.8s linear infinite;
    margin: 0 auto 15px;
}

@keyframes spin {
    to { transform: rotate(360deg); }
}

.loading p {
    color: var(--text-light);
}

/* ===== WEATHER DISPLAY ===== */
.weather-display {
    animation: fadeIn 0.5s ease;
}

@keyframes fadeIn {
    from { opacity: 0; transform: translateY(20px); }
    to { opacity: 1; transform: translateY(0); }
}

.weather-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding-bottom: 20px;
    border-bottom: 2px solid #F0F0F0;
    margin-bottom: 20px;
    flex-wrap: wrap;
    gap: 15px;
}

.weather-header h2 {
    font-size: 1.8rem;
    font-weight: 700;
    color: var(--text);
}

.weather-header p {
    color: var(--text-light);
    font-size: 0.95rem;
}

.weather-icon {
    text-align: center;
}

.weather-icon img {
    width: 70px;
    height: 70px;
}

.weather-icon p {
    font-size: 0.85rem;
    color: var(--text-light);
    text-transform: capitalize;
}

.temperature-section {
    text-align: center;
    padding: 20px 0;
}

.temperature-section h1 {
    font-size: 4.5rem;
    font-weight: 800;
    color: var(--text);
    line-height: 1;
}

.temperature-section p {
    color: var(--text-light);
    font-size: 1.1rem;
    margin-top: 5px;
}

/* ===== WEATHER DETAILS ===== */
.weather-details {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
    gap: 15px;
    margin: 20px 0;
}

.detail-card {
    background: #F8F9FA;
    border-radius: 12px;
    padding: 16px;
    text-align: center;
    transition: var(--transition);
}

.detail-card:hover {
    transform: translateY(-3px);
    box-shadow: 0 4px 12px rgba(0,0,0,0.05);
}

.detail-card i {
    font-size: 1.5rem;
    color: var(--primary);
    margin-bottom: 6px;
}

.detail-card span {
    display: block;
    font-size: 1.1rem;
    font-weight: 600;
    color: var(--text);
}

.detail-card p {
    font-size: 0.75rem;
    color: var(--text-light);
    margin-top: 2px;
    text-transform: uppercase;
    letter-spacing: 0.5px;
}

/* ===== ADDITIONAL INFO ===== */
.additional-info {
    display: flex;
    justify-content: center;
    gap: 30px;
    padding: 15px 0;
    border-top: 2px solid #F0F0F0;
    margin-top: 10px;
    flex-wrap: wrap;
}

.additional-info div {
    display: flex;
    align-items: center;
    gap: 8px;
    color: var(--text-light);
    font-size: 0.9rem;
}

.additional-info i {
    color: var(--primary);
}

.additional-info strong {
    color: var(--text);
}

/* ===== ERROR MESSAGE ===== */
.error-message {
    background: #FEE2E2;
    border: 2px solid var(--danger);
    border-radius: 12px;
    padding: 20px;
    display: flex;
    align-items: center;
    gap: 12px;
    margin-top: 15px;
    animation: shake 0.4s ease;
}

@keyframes shake {
    0%, 100% { transform: translateX(0); }
    25% { transform: translateX(-8px); }
    75% { transform: translateX(8px); }
}

.error-message i {
    font-size: 1.5rem;
    color: var(--danger);
}

.error-message p {
    color: #991B1B;
    font-weight: 500;
}

/* ===== FOOTER ===== */
footer {
    text-align: center;
    padding-top: 20px;
    margin-top: 20px;
    border-top: 2px solid #F0F0F0;
    color: var(--text-light);
    font-size: 0.8rem;
}

footer a {
    color: var(--primary);
    text-decoration: none;
    font-weight: 500;
}

footer a:hover {
    text-decoration: underline;
}

/* ===== RESPONSIVE ===== */
@media (max-width: 600px) {
    .container { padding: 24px 16px; }
    header h1 { font-size: 1.6rem; }
    .search-section { flex-direction: column; }
    .search-box { border-radius: 12px; }
    .location-btn { justify-content: center; border-radius: 12px; }
    .weather-header { flex-direction: column; text-align: center; }
    .weather-header h2 { font-size: 1.4rem; }
    .temperature-section h1 { font-size: 3.5rem; }
    .weather-details { grid-template-columns: 1fr 1fr; }
    .additional-info { flex-direction: column; align-items: center; gap: 10px; }
}

@media (max-width: 400px) {
    .weather-details { grid-template-columns: 1fr; }
}

Step 3: Add the JavaScript Logic

Create a script.js file with the following code:

JAVASCRIPT
// ===== API CONFIGURATION =====
// 🔑 Get your free API key from: https://openweathermap.org/api
// Sign up for a free account and your API key will be sent via email
const API = {
    // Option 1: OpenWeatherMap (Recommended - Get free key at openweathermap.org)
    // Free tier: 1,000 calls/day, 60 calls/minute
    key: 'd98ed33c456bd0af189da9118dac55d7', // Replace with your actual API key
    baseUrl: 'https://api.openweathermap.org/data/2.5/weather',
    units: 'metric' // 'metric' for Celsius, 'imperial' for Fahrenheit
};

// Option 2: Open-Meteo (No API key required - fallback option)
// Completely free, no registration needed
// https://open-meteo.com
const OPEN_METEO = {
    baseUrl: 'https://api.open-meteo.com/v1/forecast'
    // Use this as a fallback when OpenWeather key is not available
};

// ===== DOM ELEMENTS =====
const cityInput = document.getElementById('cityInput');
const searchBtn = document.getElementById('searchBtn');
const locationBtn = document.getElementById('locationBtn');
const weatherDisplay = document.getElementById('weatherDisplay');
const loading = document.getElementById('loading');
const errorMessage = document.getElementById('errorMessage');
const errorText = document.getElementById('errorText');
const cityName = document.getElementById('cityName');
const countryName = document.getElementById('countryName');
const temperature = document.getElementById('temperature');
const feelsLike = document.getElementById('feelsLike');
const weatherIcon = document.getElementById('weatherIcon');
const weatherDescription = document.getElementById('weatherDescription');
const humidity = document.getElementById('humidity');
const windSpeed = document.getElementById('windSpeed');
const cloudCover = document.getElementById('cloudCover');
const visibility = document.getElementById('visibility');
const uvIndex = document.getElementById('uvIndex');
const pressure = document.getElementById('pressure');
const apiSource = document.getElementById('apiSource');

// ===== HELPERS =====
function showLoading() {
    loading.classList.add('visible');
    weatherDisplay.style.display = 'none';
    errorMessage.style.display = 'none';
}

function hideLoading() {
    loading.classList.remove('visible');
}

function showError(message) {
    errorMessage.style.display = 'flex';
    errorText.textContent = message;
    weatherDisplay.style.display = 'none';
    hideLoading();
}

function hideError() {
    errorMessage.style.display = 'none';
}

function getWeatherIconUrl(iconCode) {
    return `https://openweathermap.org/img/wn/${iconCode}@4x.png`;
}

function formatTimestamp(timestamp) {
    const date = new Date(timestamp * 1000);
    return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
}

// ===== FETCH WEATHER - OpenWeatherMap =====
async function fetchWeatherByCity(city) {
    if (!city || city.trim() === '') {
        showError('Please enter a city name.');
        return;
    }

    showLoading();
    hideError();

    try {
        // Try OpenWeather first
        if (API.key && API.key !== 'YOUR_OPENWEATHER_API_KEY') {
            const url = `${API.baseUrl}?q=${encodeURIComponent(city)}&appid=${API.key}&units=${API.units}`;
            const response = await fetch(url);

            if (!response.ok) {
                if (response.status === 401) {
                    throw new Error('Invalid API key. Please check your OpenWeather API key.');
                } else if (response.status === 404) {
                    throw new Error(`City "${city}" not found. Please check the spelling.`);
                } else {
                    throw new Error('Failed to fetch weather data. Please try again.');
                }
            }

            const data = await response.json();
            displayWeather(data, 'OpenWeatherMap');
            return;
        }

        // Fallback: Use Open-Meteo (no API key required)
        // We need coordinates, so we'll use a geocoding service first
        const geoResponse = await fetch(
            `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(city)}&count=1`
        );

        if (!geoResponse.ok) {
            throw new Error('Could not find location. Please try a different city.');
        }

        const geoData = await geoResponse.json();

        if (!geoData.results || geoData.results.length === 0) {
            throw new Error(`City "${city}" not found. Please check the spelling.`);
        }

        const location = geoData.results[0];
        const lat = location.latitude;
        const lon = location.longitude;
        const name = location.name;
        const country = location.country;

        const weatherUrl = `${OPEN_METEO.baseUrl}?latitude=${lat}&longitude=${lon}&current_weather=true&temperature_unit=celsius&timezone=auto`;
        const weatherResponse = await fetch(weatherUrl);
        const weatherData = await weatherResponse.json();

        if (!weatherData.current_weather) {
            throw new Error('Could not fetch weather data.');
        }

        // Format Open-Meteo data to match OpenWeather format
        const formattedData = formatOpenMeteoData(weatherData, name, country);
        displayWeather(formattedData, 'Open-Meteo (No API Key)');

    } catch (error) {
        showError(error.message || 'Something went wrong. Please try again.');
        console.error('Weather fetch error:', error);
    } finally {
        hideLoading();
    }
}

// ===== FORMAT OPEN-METEO DATA =====
function formatOpenMeteoData(data, city, country) {
    const current = data.current_weather;

    // Map weather codes to descriptions and icons
    const weatherCodes = {
        0: { desc: 'Clear sky', icon: '01d' },
        1: { desc: 'Mainly clear', icon: '01d' },
        2: { desc: 'Partly cloudy', icon: '02d' },
        3: { desc: 'Overcast', icon: '03d' },
        45: { desc: 'Fog', icon: '50d' },
        48: { desc: 'Depositing rime fog', icon: '50d' },
        51: { desc: 'Light drizzle', icon: '09d' },
        53: { desc: 'Moderate drizzle', icon: '09d' },
        55: { desc: 'Dense drizzle', icon: '09d' },
        61: { desc: 'Slight rain', icon: '10d' },
        63: { desc: 'Moderate rain', icon: '10d' },
        65: { desc: 'Heavy rain', icon: '10d' },
        71: { desc: 'Slight snow', icon: '13d' },
        73: { desc: 'Moderate snow', icon: '13d' },
        75: { desc: 'Heavy snow', icon: '13d' },
        80: { desc: 'Slight rain showers', icon: '09d' },
        81: { desc: 'Moderate rain showers', icon: '09d' },
        82: { desc: 'Violent rain showers', icon: '09d' },
        95: { desc: 'Thunderstorm', icon: '11d' }
    };

    const weatherInfo = weatherCodes[current.weathercode] || { desc: 'Unknown', icon: '01d' };

    return {
        name: city,
        sys: { country: country || 'Unknown' },
        main: {
            temp: current.temperature,
            feels_like: current.temperature,
            humidity: current.humidity || 0,
            pressure: current.pressure || 0
        },
        weather: [{
            description: weatherInfo.desc,
            icon: weatherInfo.icon
        }],
        wind: { speed: current.windspeed || 0 },
        clouds: { all: 0 },
        visibility: 10000,
        // UV Index not available in free Open-Meteo
        uvIndex: 'N/A'
    };
}

// ===== DISPLAY WEATHER =====
function displayWeather(data, source) {
    weatherDisplay.style.display = 'block';
    hideError();
    hideLoading();

    // City & Country
    cityName.textContent = data.name || 'Unknown';
    countryName.textContent = data.sys?.country || '';

    // Temperature
    const temp = data.main?.temp;
    temperature.textContent = temp !== undefined ? `${Math.round(temp)}°C` : '--°C';

    const feelsLikeTemp = data.main?.feels_like;
    feelsLike.textContent = feelsLikeTemp !== undefined ? `Feels like ${Math.round(feelsLikeTemp)}°C` : 'Feels like --°C';

    // Weather Icon & Description
    const weather = data.weather?.[0];
    if (weather) {
        weatherIcon.src = getWeatherIconUrl(weather.icon);
        weatherIcon.alt = weather.description || 'Weather condition';
        weatherDescription.textContent = weather.description || '';
    }

    // Details
    humidity.textContent = data.main?.humidity !== undefined ? `${data.main.humidity}%` : '--%';
    windSpeed.textContent = data.wind?.speed !== undefined ? `${Math.round(data.wind.speed)} km/h` : '-- km/h';
    cloudCover.textContent = data.clouds?.all !== undefined ? `${data.clouds.all}%` : '--%';
    visibility.textContent = data.visibility !== undefined ? `${Math.round(data.visibility / 1000)} km` : '-- km';
    pressure.textContent = data.main?.pressure !== undefined ? `${data.main.pressure} hPa` : '-- hPa';

    // UV Index (not available in all APIs)
    uvIndex.textContent = data.uvIndex !== undefined ? data.uvIndex : 'N/A';

    // API Source
    apiSource.textContent = source || 'Weather API';
    apiSource.href = source === 'OpenWeatherMap'
        ? 'https://openweathermap.org'
        : source === 'Open-Meteo (No API Key)'
            ? 'https://open-meteo.com'
            : '#';
}

// ===== FETCH BY GEOLOCATION =====
async function fetchWeatherByLocation() {
    if (!navigator.geolocation) {
        showError('Geolocation is not supported by your browser.');
        return;
    }

    showLoading();
    hideError();

    navigator.geolocation.getCurrentPosition(
        async (position) => {
            const { latitude, longitude } = position.coords;

            try {
                // Try OpenWeather first
                if (API.key && API.key !== 'YOUR_OPENWEATHER_API_KEY') {
                    const url = `${API.baseUrl}?lat=${latitude}&lon=${longitude}&appid=${API.key}&units=${API.units}`;
                    const response = await fetch(url);

                    if (!response.ok) {
                        throw new Error('Failed to fetch weather for your location.');
                    }

                    const data = await response.json();
                    displayWeather(data, 'OpenWeatherMap');
                    hideLoading();
                    return;
                }

                // Fallback: Open-Meteo
                const weatherUrl = `${OPEN_METEO.baseUrl}?latitude=${latitude}&longitude=${longitude}&current_weather=true&temperature_unit=celsius&timezone=auto`;
                const response = await fetch(weatherUrl);
                const data = await response.json();

                if (!data.current_weather) {
                    throw new Error('Could not fetch weather for your location.');
                }

                // Get city name using reverse geocoding
                const geoResponse = await fetch(
                    `https://geocoding-api.open-meteo.com/v1/search?latitude=${latitude}&longitude=${longitude}&count=1`
                );

                let city = 'Your Location';
                let country = '';

                if (geoResponse.ok) {
                    const geoData = await geoResponse.json();
                    if (geoData.results && geoData.results.length > 0) {
                        city = geoData.results[0].name || city;
                        country = geoData.results[0].country || '';
                    }
                }

                const formattedData = formatOpenMeteoData(data, city, country);
                displayWeather(formattedData, 'Open-Meteo (No API Key)');
                hideLoading();

            } catch (error) {
                showError(error.message || 'Failed to get weather for your location.');
                hideLoading();
                console.error('Location weather error:', error);
            }
        },
        (error) => {
            showError('Unable to access your location. Please check your browser permissions.');
            hideLoading();
            console.error('Geolocation error:', error);
        }
    );
}

// ===== EVENT LISTENERS =====
searchBtn.addEventListener('click', () => {
    const city = cityInput.value.trim();
    if (city) {
        fetchWeatherByCity(city);
    } else {
        cityInput.focus();
        cityInput.style.borderColor = 'var(--danger)';
        setTimeout(() => {
            cityInput.style.borderColor = '';
        }, 1000);
    }
});

cityInput.addEventListener('keydown', (e) => {
    if (e.key === 'Enter') {
        e.preventDefault();
        searchBtn.click();
    }
});

locationBtn.addEventListener('click', fetchWeatherByLocation);

// ===== KEYBOARD SHORTCUT =====
document.addEventListener('keydown', (e) => {
    // Ctrl + Shift + W to focus search
    if (e.ctrlKey && e.shiftKey && (e.key === 'w' || e.key === 'W')) {
        e.preventDefault();
        cityInput.focus();
        cityInput.select();
    }
});

// ===== INITIALIZATION =====
// Check if API key is configured
if (!API.key || API.key === 'YOUR_OPENWEATHER_API_KEY') {
    console.warn('⚠️ OpenWeather API key not configured. Using Open-Meteo as fallback.');
    console.log('💡 Get a free API key at: https://openweathermap.org/api');
    console.log('📖 Free tier: 1,000 calls/day, 60 calls/minute');
    apiSource.textContent = 'Open-Meteo (No API Key)';
} else {
    console.log('✅ OpenWeather API key configured.');
}

// Load default city
const defaultCity = 'London';
fetchWeatherByCity(defaultCity);

// Set focus to input
cityInput.focus();

console.log('🌤️ Weather Dashboard loaded!');
console.log('💡 Tip: Use Ctrl+Shift+W to focus the search input');

How This Weather Dashboard Works

API integration: fetchWeatherByCity() sends a request to the OpenWeather API using the city name and API key, then parses the JSON response for display. If no OpenWeather key is configured, it automatically falls back to Open-Meteo, which needs no key at all.

Geolocation: fetchWeatherByLocation() uses the browser's Geolocation API to get the user's coordinates, then fetches weather for that exact position.

Dynamic display: displayWeather() updates every element on the page — temperature, humidity, wind speed, icons — from the fetched data.

Error handling: invalid city names, network failures, and API errors all surface a clear, user-friendly message instead of a silent failure.

Final Output

Once all three files are in place and your API key is added, the dashboard will:

  • Display current weather conditions for any city
  • Show temperature, humidity, wind speed, and more
  • Update instantly when searching for a new city
  • Support geolocation for local weather
  • Be fully responsive on all devices
  • Handle errors gracefully with clear messages

Getting Your API Key: Step-by-Step (OpenWeatherMap)

  1. Go to OpenWeatherMap: Visit openweathermap.org/api
  2. Sign up: Click "Sign Up" and create a free account
  3. Verify your email: Check your inbox for a verification link
  4. Get your API key: After logging in, open the "API keys" tab in your account dashboard
  5. Wait for activation: New keys typically activate within 10 minutes to 2 hours
  6. Replace it in the code: Swap YOUR_OPENWEATHER_API_KEY in script.js for your real key

Quick Comparison of Free Weather APIs

API Free Tier API Key Required Best For
OpenWeatherMap 1,000 calls/day, 60/min Yes Most popular, well-documented
Open-Meteo Unlimited, completely free No No registration, ideal for beginners
WeatherAPI.com 1,000,000 calls/month Yes High free-tier limits
Visual Crossing 1,000 records/day Yes Historical weather data

Customization Guide

1. Change Temperature Units

JAVASCRIPT
// In script.js
const API = {
    key: 'YOUR_API_KEY',
    baseUrl: 'https://api.openweathermap.org/data/2.5/weather',
    units: 'imperial' // Change to 'imperial' for Fahrenheit
};

2. Add a 5-Day Forecast

JAVASCRIPT
// Add a forecast endpoint
const forecastUrl = `https://api.openweathermap.org/data/2.5/forecast?q=${city}&appid=${API.key}&units=${API.units}`;

3. Change the Default City

JAVASCRIPT
// In script.js - at the bottom
const defaultCity = 'New York'; // Change to your preferred city

4. Add a Background Based on Conditions

CSS
/* In style.css - add dynamic backgrounds based on weather */
.weather-display.clear-sky {
    background: linear-gradient(135deg, #FDBB2D, #FDC830);
}
.weather-display.rainy {
    background: linear-gradient(135deg, #2C3E50, #3498DB);
}

Troubleshooting Guide

Issue Solution
API key not working Wait up to a couple of hours for activation, and double-check for typos
City not found Check spelling and use the full city name (e.g. "New York" rather than "NY")
Rate limit exceeded The free tier caps at 60 calls/minute — wait a moment and try again
Geolocation not working Check browser permissions and allow location access
No data displayed Check your internet connection and verify the API key is correct

Future Enhancements

  • 5-day weather forecast
  • Weather alerts and notifications
  • Air quality index (AQI) data
  • Saved favorites using local storage
  • Weather maps visualization
  • Dark/light theme toggle
  • Hourly forecast breakdown
  • Multiple language support

Conclusion

This project covers the core skills behind most real-world front-end apps: fetching live data from a REST API, handling asynchronous requests with async/await, parsing and rendering JSON dynamically, managing errors gracefully, and building a responsive layout that adapts to any screen. With the OpenWeather integration and the Open-Meteo fallback in place, you now have a dashboard that can pull real-time weather for anywhere in the world.

Technologies Used

HTML5 CSS3 JavaScript

Live Output

See the project in action below. This is the live preview of the code you just learned.

Output Preview