Build a Weather Application

Weekend Project: Create a Real-Time Weather App Using OpenWeatherMap API

Project Overview

Welcome to your first API-based project! Today, we're building a weather application that fetches real-time weather data. Think of it like creating your own mini weather channel. Users will enter a city name, and your app will display current weather conditions, temperature, humidity, and more. This project combines everything you've learned this week: DOM manipulation, event handling, asynchronous JavaScript, and error handling.

Project Requirements

Using George Polya's Problem-Solving Method

Step 1: Understand the Problem

We need to create a weather application that:

Step 2: Devise a Plan

  1. Set up the project structure (HTML, CSS, JavaScript files)
  2. Create the HTML layout with necessary elements
  3. Style the application with CSS
  4. Get an API key from OpenWeatherMap
  5. Write JavaScript to:
    • Handle form submission
    • Make API requests
    • Process and display data
    • Handle errors
    • Add temperature unit conversion
  6. Test with different cities and error scenarios
  7. Add final touches and improvements

Step 3: Carry Out the Plan

Let's implement our weather application step by step.

Step 4: Look Back and Reflect

After completing the project, we'll review what we've learned and consider improvements.

Project Structure

graph TD A[weather-app/] --> B[index.html] A --> C[styles/] A --> D[js/] C --> E[style.css] D --> F[app.js] D --> G[config.js] A --> H[images/] H --> I[weather-icons/]

Step 1: Create the HTML Structure

File: /weather-app/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 App</title>
    <link rel="stylesheet" href="styles/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 class="header">
            <h1><i class="fas fa-cloud-sun"></i> Weather App</h1>
        </header>
        
        <main class="main">
            <div class="search-box">
                <form id="searchForm">
                    <input 
                        type="text" 
                        id="cityInput" 
                        placeholder="Enter city name..." 
                        autocomplete="off"
                        required
                    >
                    <button type="submit" id="searchBtn">
                        <i class="fas fa-search"></i>
                    </button>
                </form>
            </div>
            
            <div class="loading" id="loading" style="display: none;">
                <i class="fas fa-spinner fa-spin"></i>
                <p>Loading weather data...</p>
            </div>
            
            <div class="error" id="error" style="display: none;">
                <i class="fas fa-exclamation-circle"></i>
                <p id="errorMessage"></p>
            </div>
            
            <div class="weather-card" id="weatherCard" style="display: none;">
                <div class="weather-header">
                    <h2 id="cityName"></h2>
                    <p id="dateTime"></p>
                </div>
                
                <div class="weather-body">
                    <div class="temperature">
                        <img id="weatherIcon" src="" alt="Weather icon">
                        <div class="temp-main">
                            <span id="temperature"></span>
                            <div class="temp-toggle">
                                <button class="active" id="celsiusBtn">°C</button>
                                <button id="fahrenheitBtn">°F</button>
                            </div>
                        </div>
                    </div>
                    
                    <div class="weather-description">
                        <p id="description"></p>
                        <p id="feelsLike">Feels like: <span></span></p>
                    </div>
                </div>
                
                <div class="weather-details">
                    <div class="detail">
                        <i class="fas fa-tint"></i>
                        <p>Humidity</p>
                        <p id="humidity"></p>
                    </div>
                    <div class="detail">
                        <i class="fas fa-wind"></i>
                        <p>Wind Speed</p>
                        <p id="windSpeed"></p>
                    </div>
                    <div class="detail">
                        <i class="fas fa-compress-arrows-alt"></i>
                        <p>Pressure</p>
                        <p id="pressure"></p>
                    </div>
                </div>
            </div>
        </main>
        
        <footer class="footer">
            <p>Data provided by OpenWeatherMap</p>
        </footer>
    </div>
    
    <script src="js/config.js"></script>
    <script src="js/app.js"></script>
</body>
</html>

Step 2: Create the CSS Styles

File: /weather-app/styles/style.css

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    background: linear-gradient(135deg, #74ebd5 0%, #acb6e5 100%);
    min-height: 100vh;
    color: #333;
}

.container {
    max-width: 800px;
    margin: 0 auto;
    padding: 20px;
}

.header {
    text-align: center;
    margin-bottom: 30px;
    color: white;
}

.header h1 {
    font-size: 2.5rem;
    text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
}

.search-box {
    margin-bottom: 30px;
}

#searchForm {
    display: flex;
    max-width: 500px;
    margin: 0 auto;
}

#cityInput {
    flex: 1;
    padding: 15px;
    font-size: 1.1rem;
    border: none;
    border-radius: 25px 0 0 25px;
    outline: none;
}

#searchBtn {
    padding: 15px 25px;
    font-size: 1.1rem;
    background: #4a90e2;
    color: white;
    border: none;
    border-radius: 0 25px 25px 0;
    cursor: pointer;
    transition: background 0.3s;
}

#searchBtn:hover {
    background: #357abd;
}

.loading, .error {
    text-align: center;
    padding: 20px;
    margin: 20px auto;
    max-width: 500px;
    border-radius: 10px;
}

.loading {
    background: rgba(255, 255, 255, 0.9);
}

.error {
    background: #ffebee;
    color: #c62828;
}

.weather-card {
    background: rgba(255, 255, 255, 0.95);
    border-radius: 20px;
    padding: 30px;
    max-width: 600px;
    margin: 0 auto;
    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}

.weather-header {
    text-align: center;
    margin-bottom: 20px;
}

.weather-header h2 {
    font-size: 2rem;
    margin-bottom: 5px;
}

.temperature {
    display: flex;
    align-items: center;
    justify-content: center;
    margin: 20px 0;
}

.temp-main {
    display: flex;
    align-items: center;
    gap: 10px;
}

#temperature {
    font-size: 4rem;
    font-weight: bold;
}

.temp-toggle {
    display: flex;
    flex-direction: column;
    gap: 5px;
}

.temp-toggle button {
    padding: 5px 10px;
    border: 1px solid #ddd;
    background: white;
    cursor: pointer;
    border-radius: 5px;
}

.temp-toggle button.active {
    background: #4a90e2;
    color: white;
    border-color: #4a90e2;
}

.weather-description {
    text-align: center;
    margin: 20px 0;
}

#description {
    font-size: 1.5rem;
    text-transform: capitalize;
    margin-bottom: 10px;
}

.weather-details {
    display: flex;
    justify-content: space-around;
    margin-top: 30px;
    padding-top: 20px;
    border-top: 1px solid #eee;
}

.detail {
    text-align: center;
}

.detail i {
    font-size: 1.5rem;
    color: #4a90e2;
    margin-bottom: 10px;
}

.footer {
    text-align: center;
    margin-top: 50px;
    color: white;
    text-shadow: 1px 1px 2px rgba(0,0,0,0.2);
}

@media (max-width: 600px) {
    .weather-details {
        flex-direction: column;
        gap: 20px;
    }
    
    #temperature {
        font-size: 3rem;
    }
    
    .header h1 {
        font-size: 2rem;
    }
}

Step 3: Create the Configuration File

File: /weather-app/js/config.js

// OpenWeatherMap API configuration
const API_KEY = 'YOUR_API_KEY_HERE'; // Replace with your actual API key
const BASE_URL = 'https://api.openweathermap.org/data/2.5/weather';

// To get an API key:
// 1. Go to https://openweathermap.org/
// 2. Sign up for a free account
// 3. Go to API keys section in your account
// 4. Generate a new API key
// 5. Replace 'YOUR_API_KEY_HERE' with your actual key

Step 4: Create the Main JavaScript File

File: /weather-app/js/app.js

// DOM Elements
const searchForm = document.getElementById('searchForm');
const cityInput = document.getElementById('cityInput');
const weatherCard = document.getElementById('weatherCard');
const loading = document.getElementById('loading');
const error = document.getElementById('error');
const errorMessage = document.getElementById('errorMessage');

// Weather data elements
const cityName = document.getElementById('cityName');
const dateTime = document.getElementById('dateTime');
const temperature = document.getElementById('temperature');
const weatherIcon = document.getElementById('weatherIcon');
const description = document.getElementById('description');
const feelsLike = document.querySelector('#feelsLike span');
const humidity = document.getElementById('humidity');
const windSpeed = document.getElementById('windSpeed');
const pressure = document.getElementById('pressure');

// Temperature toggle buttons
const celsiusBtn = document.getElementById('celsiusBtn');
const fahrenheitBtn = document.getElementById('fahrenheitBtn');

// State
let currentWeatherData = null;
let isCelsius = true;

// Event Listeners
searchForm.addEventListener('submit', handleSearch);
celsiusBtn.addEventListener('click', () => toggleTemperatureUnit(true));
fahrenheitBtn.addEventListener('click', () => toggleTemperatureUnit(false));

// Handle search form submission
async function handleSearch(e) {
    e.preventDefault();
    
    const city = cityInput.value.trim();
    if (!city) return;
    
    // Reset UI
    showLoading();
    hideError();
    hideWeatherCard();
    
    try {
        const data = await fetchWeatherData(city);
        currentWeatherData = data;
        displayWeatherData(data);
        showWeatherCard();
    } catch (err) {
        showError(err.message);
    } finally {
        hideLoading();
    }
}

// Fetch weather data from API
async function fetchWeatherData(city) {
    const url = `${BASE_URL}?q=${encodeURIComponent(city)}&appid=${API_KEY}&units=metric`;
    
    try {
        const response = await fetch(url);
        
        if (!response.ok) {
            if (response.status === 404) {
                throw new Error('City not found. Please check the spelling and try again.');
            } else if (response.status === 401) {
                throw new Error('Invalid API key. Please check your configuration.');
            } else {
                throw new Error('Failed to fetch weather data. Please try again later.');
            }
        }
        
        const data = await response.json();
        return data;
    } catch (err) {
        if (err.message.includes('Failed to fetch')) {
            throw new Error('Network error. Please check your internet connection.');
        }
        throw err;
    }
}

// Display weather data in the UI
function displayWeatherData(data) {
    // Update city name and date
    cityName.textContent = `${data.name}, ${data.sys.country}`;
    dateTime.textContent = new Date().toLocaleDateString('en-US', {
        weekday: 'long',
        year: 'numeric',
        month: 'long',
        day: 'numeric',
        hour: '2-digit',
        minute: '2-digit'
    });
    
    // Update temperature
    updateTemperatureDisplay(data.main.temp);
    
    // Update weather icon
    const iconCode = data.weather[0].icon;
    weatherIcon.src = `https://openweathermap.org/img/wn/${iconCode}@2x.png`;
    weatherIcon.alt = data.weather[0].description;
    
    // Update description
    description.textContent = data.weather[0].description;
    
    // Update other details
    updateFeelsLikeDisplay(data.main.feels_like);
    humidity.textContent = `${data.main.humidity}%`;
    windSpeed.textContent = `${data.wind.speed} m/s`;
    pressure.textContent = `${data.main.pressure} hPa`;
}

// Temperature conversion functions
function celsiusToFahrenheit(celsius) {
    return (celsius * 9/5) + 32;
}

function updateTemperatureDisplay(tempCelsius) {
    if (isCelsius) {
        temperature.textContent = `${Math.round(tempCelsius)}°C`;
    } else {
        temperature.textContent = `${Math.round(celsiusToFahrenheit(tempCelsius))}°F`;
    }
}

function updateFeelsLikeDisplay(feelsLikeCelsius) {
    if (isCelsius) {
        feelsLike.textContent = `${Math.round(feelsLikeCelsius)}°C`;
    } else {
        feelsLike.textContent = `${Math.round(celsiusToFahrenheit(feelsLikeCelsius))}°F`;
    }
}

// Toggle temperature unit
function toggleTemperatureUnit(toCelsius) {
    if (isCelsius === toCelsius) return;
    
    isCelsius = toCelsius;
    
    // Update button states
    if (isCelsius) {
        celsiusBtn.classList.add('active');
        fahrenheitBtn.classList.remove('active');
    } else {
        celsiusBtn.classList.remove('active');
        fahrenheitBtn.classList.add('active');
    }
    
    // Update temperature displays if we have data
    if (currentWeatherData) {
        updateTemperatureDisplay(currentWeatherData.main.temp);
        updateFeelsLikeDisplay(currentWeatherData.main.feels_like);
    }
}

// UI state management functions
function showLoading() {
    loading.style.display = 'block';
}

function hideLoading() {
    loading.style.display = 'none';
}

function showError(message) {
    errorMessage.textContent = message;
    error.style.display = 'block';
}

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

function showWeatherCard() {
    weatherCard.style.display = 'block';
}

function hideWeatherCard() {
    weatherCard.style.display = 'none';
}

// Initialize the app
function init() {
    // Clear input on page load
    cityInput.value = '';
    
    // Hide all cards initially
    hideLoading();
    hideError();
    hideWeatherCard();
    
    // Focus on input
    cityInput.focus();
}

// Run initialization
init();

Application Flow Diagram

graph TD A[User enters city name] --> B[Form submission] B --> C[Show loading state] C --> D[Fetch weather data from API] D --> E{API Response} E -->|Success| F[Parse JSON data] E -->|Error| G[Show error message] F --> H[Update UI with weather data] H --> I[Hide loading state] G --> I I --> J[User can toggle temperature units] J --> K[Update temperature display]

API Response Structure

// Example API response from OpenWeatherMap
{
  "coord": { "lon": -0.1257, "lat": 51.5085 },
  "weather": [
    {
      "id": 802,
      "main": "Clouds",
      "description": "scattered clouds",
      "icon": "03d"
    }
  ],
  "base": "stations",
  "main": {
    "temp": 15.5,
    "feels_like": 14.8,
    "temp_min": 13.9,
    "temp_max": 16.7,
    "pressure": 1013,
    "humidity": 67
  },
  "visibility": 10000,
  "wind": {
    "speed": 4.1,
    "deg": 250
  },
  "clouds": { "all": 40 },
  "dt": 1635701200,
  "sys": {
    "type": 2,
    "id": 2019646,
    "country": "GB",
    "sunrise": 1635663371,
    "sunset": 1635699467
  },
  "timezone": 0,
  "id": 2643743,
  "name": "London",
  "cod": 200
}

Error Handling Scenarios

// Enhanced error handling
async function fetchWeatherData(city) {
    try {
        const response = await fetch(url);
        
        if (!response.ok) {
            switch (response.status) {
                case 404:
                    throw new Error('City not found. Please check the spelling.');
                case 401:
                    throw new Error('Invalid API key. Please check configuration.');
                case 429:
                    throw new Error('Too many requests. Please try again later.');
                case 500:
                case 502:
                case 503:
                case 504:
                    throw new Error('Server error. Please try again later.');
                default:
                    throw new Error('Failed to fetch weather data.');
            }
        }
        
        return await response.json();
    } catch (err) {
        if (err instanceof TypeError && err.message.includes('Failed to fetch')) {
            throw new Error('Network error. Check your internet connection.');
        }
        throw err;
    }
}

Additional Features to Consider

Implementing Geolocation Feature

// Add geolocation button to HTML
<button id="locationBtn" title="Use my location">
    <i class="fas fa-location-arrow"></i>
</button>

// Add geolocation functionality
const locationBtn = document.getElementById('locationBtn');

locationBtn.addEventListener('click', getLocationWeather);

function getLocationWeather() {
    if (!navigator.geolocation) {
        showError('Geolocation is not supported by your browser');
        return;
    }
    
    showLoading();
    navigator.geolocation.getCurrentPosition(
        async (position) => {
            const { latitude, longitude } = position.coords;
            try {
                const data = await fetchWeatherByCoords(latitude, longitude);
                currentWeatherData = data;
                displayWeatherData(data);
                showWeatherCard();
            } catch (err) {
                showError(err.message);
            } finally {
                hideLoading();
            }
        },
        (error) => {
            hideLoading();
            switch(error.code) {
                case error.PERMISSION_DENIED:
                    showError('Location permission denied');
                    break;
                case error.POSITION_UNAVAILABLE:
                    showError('Location information unavailable');
                    break;
                case error.TIMEOUT:
                    showError('Location request timed out');
                    break;
                default:
                    showError('An unknown error occurred');
            }
        }
    );
}

async function fetchWeatherByCoords(lat, lon) {
    const url = `${BASE_URL}?lat=${lat}&lon=${lon}&appid=${API_KEY}&units=metric`;
    const response = await fetch(url);
    
    if (!response.ok) {
        throw new Error('Failed to fetch weather data');
    }
    
    return await response.json();
}

Adding Local Storage for Search History

// Search history functions
function saveToHistory(city) {
    let history = JSON.parse(localStorage.getItem('weatherSearchHistory') || '[]');
    
    // Remove if already exists
    history = history.filter(item => item.toLowerCase() !== city.toLowerCase());
    
    // Add to beginning
    history.unshift(city);
    
    // Keep only last 5 searches
    history = history.slice(0, 5);
    
    localStorage.setItem('weatherSearchHistory', JSON.stringify(history));
    updateHistoryDisplay();
}

function loadHistory() {
    return JSON.parse(localStorage.getItem('weatherSearchHistory') || '[]');
}

function updateHistoryDisplay() {
    const history = loadHistory();
    const historyContainer = document.getElementById('searchHistory');
    
    if (history.length === 0) {
        historyContainer.style.display = 'none';
        return;
    }
    
    historyContainer.style.display = 'block';
    historyContainer.innerHTML = `
        <h3>Recent Searches</h3>
        <div class="history-list">
            ${history.map(city => `
                <button class="history-item" data-city="${city}">
                    ${city}
                </button>
            `).join('')}
        </div>
    `;
    
    // Add click handlers
    document.querySelectorAll('.history-item').forEach(button => {
        button.addEventListener('click', () => {
            cityInput.value = button.dataset.city;
            searchForm.dispatchEvent(new Event('submit'));
        });
    });
}

Testing Your Application

Test Cases to Consider:

Deployment Considerations

Common Issues and Solutions

Issue Possible Cause Solution
API returns 401 Invalid API key Check API key in config.js
CORS errors Browser security policy Use the CORS-enabled API endpoint
Weather icon not loading HTTP/HTTPS mismatch Use https:// for icon URLs
Geolocation not working Not on HTTPS Deploy on HTTPS or use localhost
Temperature not updating State not managed correctly Check currentWeatherData state

Best Practices Applied

Key Learning Outcomes

Next Steps and Enhancements

Resources