Daily Tech Brief

Top startup stories in your inbox

Subscribe Free

Β© 2026 rakrisi Daily

Project 2 - Weather Dashboard

Project 2: Weather Dashboard

Welcome to your second complete Python application! We’re building a beautiful web-based weather dashboard that displays current conditions, forecasts, and interactive charts using real weather data.

Project Overview

WeatherWise is a full-stack web application that provides:

  • 🌀️ Real-time weather conditions for any location
  • πŸ“Š 7-day weather forecast with interactive charts
  • πŸ“ GPS-based location detection
  • 🎨 Modern, responsive web interface
  • ⭐ Favorite cities management
  • πŸ”„ Auto-refresh weather data
  • πŸ“± Mobile-friendly design

Learning Objectives

By the end of this project, you’ll be able to:

  • Build full-stack web applications with Flask
  • Integrate external APIs and handle authentication
  • Design and implement database schemas
  • Create responsive web interfaces with HTML/CSS/JavaScript
  • Implement proper error handling and logging
  • Deploy web applications for public access

Project Requirements

Core Features

  1. Current Weather Display

    • Temperature, humidity, wind speed, conditions
    • Weather icons and descriptions
    • Last updated timestamp
    • Location coordinates
  2. Weather Forecast

    • 7-day forecast with daily highs/lows
    • Hourly forecast for current day
    • Weather condition probabilities
    • Interactive charts and graphs
  3. Location Management

    • Search cities by name
    • GPS-based current location detection
    • Favorite cities for quick access
    • Location history and preferences
  4. Data Visualization

    • Temperature trend charts
    • Precipitation forecasts
    • Wind speed and direction
    • Weather condition summaries

Advanced Features

  1. User Experience

    • Responsive design for all devices
    • Dark/light theme toggle
    • Loading animations and transitions
    • Error handling with user-friendly messages
  2. Data Management

    • SQLite database for user preferences
    • Weather data caching for performance
    • Favorite cities persistence
    • Search history
  3. API Integration

    • OpenWeatherMap API integration
    • Geocoding for location search
    • Weather icons and images
    • Error handling for API failures

Project Structure

weatherwise/
β”œβ”€β”€ app/
β”‚   β”œβ”€β”€ __init__.py          # Flask application factory
β”‚   β”œβ”€β”€ routes.py            # Web routes and view functions
β”‚   β”œβ”€β”€ models.py            # Database models
β”‚   β”œβ”€β”€ weather_api.py       # Weather API integration
β”‚   β”œβ”€β”€ forms.py             # Web forms
β”‚   └── utils.py             # Helper functions
β”œβ”€β”€ templates/
β”‚   β”œβ”€β”€ base.html            # Base template
β”‚   β”œβ”€β”€ index.html           # Home page
β”‚   β”œβ”€β”€ city.html            # City weather page
β”‚   β”œβ”€β”€ favorites.html       # Favorites page
β”‚   └── error.html           # Error page
β”œβ”€β”€ static/
β”‚   β”œβ”€β”€ css/
β”‚   β”‚   β”œβ”€β”€ style.css        # Main stylesheet
β”‚   β”‚   └── responsive.css   # Mobile styles
β”‚   β”œβ”€β”€ js/
β”‚   β”‚   β”œβ”€β”€ weather.js       # Weather functionality
β”‚   β”‚   β”œβ”€β”€ charts.js        # Chart visualizations
β”‚   β”‚   └── app.js           # General app functionality
β”‚   └── img/
β”‚       β”œβ”€β”€ icons/           # Weather icons
β”‚       └── logo.png
β”œβ”€β”€ migrations/               # Database migrations
β”œβ”€β”€ tests/
β”‚   β”œβ”€β”€ test_weather_api.py
β”‚   β”œβ”€β”€ test_routes.py
β”‚   └── test_models.py
β”œβ”€β”€ config.py                 # Configuration settings
β”œβ”€β”€ requirements.txt          # Python dependencies
β”œβ”€β”€ run.py                    # Application entry point
└── README.md                 # Documentation

Step 1: Set Up Flask Application

Let’s start by creating the Flask application structure.

# config.py
import os
from datetime import timedelta

class Config:
    """Base configuration."""
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production'
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///weatherwise.db'
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    
    # Weather API Configuration
    WEATHER_API_KEY = os.environ.get('WEATHER_API_KEY') or 'your-api-key-here'
    WEATHER_API_BASE_URL = 'https://api.openweathermap.org/data/2.5'
    WEATHER_ICON_URL = 'https://openweathermap.org/img/wn/{icon}@2x.png'
    
    # Cache settings
    CACHE_TYPE = 'simple'
    CACHE_DEFAULT_TIMEOUT = 300  # 5 minutes
    
    # Session settings
    PERMANENT_SESSION_LIFETIME = timedelta(days=7)

class DevelopmentConfig(Config):
    """Development configuration."""
    DEBUG = True
    SQLALCHEMY_ECHO = True

class ProductionConfig(Config):
    """Production configuration."""
    DEBUG = False
    # Add production-specific settings here

config = {
    'development': DevelopmentConfig,
    'production': ProductionConfig,
    'default': DevelopmentConfig
}
# app/__init__.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_caching import Cache
import click

db = SQLAlchemy()
cache = Cache()

def create_app(config_name='default'):
    """Application factory function."""
    app = Flask(__name__)
    
    # Load configuration
    from config import config
    app.config.from_object(config[config_name])
    
    # Initialize extensions
    db.init_app(app)
    cache.init_app(app)
    
    # Register blueprints
    from app.routes import main_bp
    app.register_blueprint(main_bp)
    
    # Register CLI commands
    register_cli_commands(app)
    
    return app

def register_cli_commands(app):
    """Register Flask CLI commands."""
    
    @app.cli.command('init-db')
    def init_db_command():
        """Initialize the database."""
        db.create_all()
        click.echo('Database initialized.')
    
    @app.cli.command('create-admin')
    @click.argument('username')
    @click.argument('email')
    def create_admin_command(username, email):
        """Create an admin user."""
        from app.models import User
        user = User(username=username, email=email, is_admin=True)
        user.set_password('admin123')  # Change in production
        db.session.add(user)
        db.session.commit()
        click.echo(f'Admin user {username} created.')

if __name__ == '__main__':
    app = create_app()
    app.run()
# run.py
#!/usr/bin/env python3
"""
WeatherWise - Weather Dashboard Application
"""

import os
from app import create_app

app = create_app(os.environ.get('FLASK_ENV') or 'development')

if __name__ == '__main__':
    app.run(
        host='0.0.0.0',
        port=int(os.environ.get('PORT', 5000)),
        debug=app.config['DEBUG']
    )

Step 2: Create Database Models

Design the database schema for user preferences and weather data.

# app/models.py
from datetime import datetime
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import UserMixin
from app import db

class User(UserMixin, db.Model):
    """User model for authentication and preferences."""
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(64), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)
    password_hash = db.Column(db.String(128))
    is_admin = db.Column(db.Boolean, default=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    
    # User preferences
    temperature_unit = db.Column(db.String(10), default='celsius')  # celsius, fahrenheit
    theme = db.Column(db.String(10), default='light')  # light, dark
    default_location = db.Column(db.String(100))  # Default city name
    
    # Relationships
    favorite_cities = db.relationship('FavoriteCity', backref='user', lazy='dynamic')
    
    def set_password(self, password):
        """Set user password hash."""
        self.password_hash = generate_password_hash(password)
    
    def check_password(self, password):
        """Check user password."""
        return check_password_hash(self.password_hash, password)
    
    def __repr__(self):
        return f'<User {self.username}>'

class FavoriteCity(db.Model):
    """User's favorite cities for quick access."""
    id = db.Column(db.Integer, primary_key=True)
    city_name = db.Column(db.String(100), nullable=False)
    country_code = db.Column(db.String(10))
    latitude = db.Column(db.Float)
    longitude = db.Column(db.Float)
    added_at = db.Column(db.DateTime, default=datetime.utcnow)
    
    # Foreign key
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    
    def __repr__(self):
        return f'<FavoriteCity {self.city_name}>'

class WeatherCache(db.Model):
    """Cache for weather API responses."""
    id = db.Column(db.Integer, primary_key=True)
    cache_key = db.Column(db.String(200), unique=True, nullable=False)
    data = db.Column(db.Text, nullable=False)  # JSON data
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    expires_at = db.Column(db.DateTime, nullable=False)
    
    def is_expired(self):
        """Check if cache entry is expired."""
        return datetime.utcnow() > self.expires_at
    
    @staticmethod
    def clean_expired():
        """Remove expired cache entries."""
        WeatherCache.query.filter(WeatherCache.expires_at < datetime.utcnow()).delete()
        db.session.commit()

class SearchHistory(db.Model):
    """Track user search history."""
    id = db.Column(db.Integer, primary_key=True)
    query = db.Column(db.String(200), nullable=False)
    result_count = db.Column(db.Integer, default=0)
    searched_at = db.Column(db.DateTime, default=datetime.utcnow)
    
    # Foreign key (optional - can be anonymous)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
    
    def __repr__(self):
        return f'<SearchHistory {self.query}>'

Step 3: Weather API Integration

Create the weather API client to fetch real weather data.

# app/weather_api.py
import requests
import json
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Tuple
from flask import current_app
from app.models import WeatherCache
from app import db

class WeatherAPI:
    """OpenWeatherMap API client."""
    
    def __init__(self):
        self.api_key = current_app.config['WEATHER_API_KEY']
        self.base_url = current_app.config['WEATHER_API_BASE_URL']
        self.icon_url_template = current_app.config['WEATHER_ICON_URL']
    
    def get_current_weather(self, lat: float, lon: float) -> Optional[Dict]:
        """Get current weather for coordinates."""
        cache_key = f"current_{lat}_{lon}"
        cached_data = self._get_cached_data(cache_key)
        if cached_data:
            return cached_data
        
        url = f"{self.base_url}/weather"
        params = {
            'lat': lat,
            'lon': lon,
            'appid': self.api_key,
            'units': 'metric'
        }
        
        try:
            response = requests.get(url, params=params, timeout=10)
            response.raise_for_status()
            data = response.json()
            
            # Cache the result
            self._cache_data(cache_key, data, minutes=10)
            
            return self._format_current_weather(data)
        except requests.RequestException as e:
            current_app.logger.error(f"Weather API error: {e}")
            return None
    
    def get_weather_forecast(self, lat: float, lon: float) -> Optional[Dict]:
        """Get 7-day weather forecast."""
        cache_key = f"forecast_{lat}_{lon}"
        cached_data = self._get_cached_data(cache_key)
        if cached_data:
            return cached_data
        
        url = f"{self.base_url}/onecall"
        params = {
            'lat': lat,
            'lon': lon,
            'exclude': 'minutely,alerts',
            'appid': self.api_key,
            'units': 'metric'
        }
        
        try:
            response = requests.get(url, params=params, timeout=10)
            response.raise_for_status()
            data = response.json()
            
            # Cache the result
            self._cache_data(cache_key, data, minutes=30)
            
            return self._format_forecast(data)
        except requests.RequestException as e:
            current_app.logger.error(f"Forecast API error: {e}")
            return None
    
    def geocode_city(self, city_name: str) -> List[Dict]:
        """Geocode city name to coordinates."""
        cache_key = f"geocode_{city_name.lower()}"
        cached_data = self._get_cached_data(cache_key)
        if cached_data:
            return cached_data
        
        url = "https://api.openweathermap.org/geo/1.0/direct"
        params = {
            'q': city_name,
            'limit': 5,
            'appid': self.api_key
        }
        
        try:
            response = requests.get(url, params=params, timeout=10)
            response.raise_for_status()
            data = response.json()
            
            # Cache the result
            self._cache_data(cache_key, data, hours=24)
            
            return self._format_geocode_results(data)
        except requests.RequestException as e:
            current_app.logger.error(f"Geocoding API error: {e}")
            return []
    
    def reverse_geocode(self, lat: float, lon: float) -> Optional[str]:
        """Get city name from coordinates."""
        cache_key = f"reverse_{lat}_{lon}"
        cached_data = self._get_cached_data(cache_key)
        if cached_data:
            return cached_data
        
        url = "https://api.openweathermap.org/geo/1.0/reverse"
        params = {
            'lat': lat,
            'lon': lon,
            'limit': 1,
            'appid': self.api_key
        }
        
        try:
            response = requests.get(url, params=params, timeout=10)
            response.raise_for_status()
            data = response.json()
            
            if data:
                city_name = f"{data[0]['name']}, {data[0]['country']}"
                # Cache the result
                self._cache_data(cache_key, city_name, hours=24)
                return city_name
        except requests.RequestException as e:
            current_app.logger.error(f"Reverse geocoding API error: {e}")
        
        return None
    
    def _get_cached_data(self, cache_key: str) -> Optional[Dict]:
        """Get data from cache if available and not expired."""
        cache_entry = WeatherCache.query.filter_by(cache_key=cache_key).first()
        if cache_entry and not cache_entry.is_expired():
            try:
                return json.loads(cache_entry.data)
            except json.JSONDecodeError:
                pass
        return None
    
    def _cache_data(self, cache_key: str, data: Dict, minutes: int = 10, hours: int = 0):
        """Cache data with expiration time."""
        expires_at = datetime.utcnow() + timedelta(minutes=minutes, hours=hours)
        
        # Remove existing cache entry
        WeatherCache.query.filter_by(cache_key=cache_key).delete()
        
        # Add new cache entry
        cache_entry = WeatherCache(
            cache_key=cache_key,
            data=json.dumps(data),
            expires_at=expires_at
        )
        db.session.add(cache_entry)
        db.session.commit()
    
    def _format_current_weather(self, data: Dict) -> Dict:
        """Format current weather data for frontend."""
        return {
            'location': {
                'name': data['name'],
                'country': data['sys']['country'],
                'lat': data['coord']['lat'],
                'lon': data['coord']['lon']
            },
            'weather': {
                'main': data['weather'][0]['main'],
                'description': data['weather'][0]['description'],
                'icon': self.icon_url_template.format(icon=data['weather'][0]['icon'])
            },
            'temperature': {
                'current': round(data['main']['temp']),
                'feels_like': round(data['main']['feels_like']),
                'min': round(data['main']['temp_min']),
                'max': round(data['main']['temp_max'])
            },
            'details': {
                'humidity': data['main']['humidity'],
                'pressure': data['main']['pressure'],
                'wind_speed': data['wind']['speed'],
                'wind_direction': data['wind'].get('deg', 0),
                'visibility': data.get('visibility', 0) / 1000,  # Convert to km
                'clouds': data['clouds']['all']
            },
            'sun': {
                'sunrise': datetime.fromtimestamp(data['sys']['sunrise']).strftime('%H:%M'),
                'sunset': datetime.fromtimestamp(data['sys']['sunset']).strftime('%H:%M')
            },
            'updated_at': datetime.now().isoformat()
        }
    
    def _format_forecast(self, data: Dict) -> Dict:
        """Format forecast data for frontend."""
        # Current hour details
        hourly = []
        for hour in data['hourly'][:24]:  # Next 24 hours
            hourly.append({
                'time': datetime.fromtimestamp(hour['dt']).strftime('%H:%M'),
                'temp': round(hour['temp']),
                'feels_like': round(hour['feels_like']),
                'humidity': hour['humidity'],
                'wind_speed': hour['wind_speed'],
                'weather': {
                    'main': hour['weather'][0]['main'],
                    'description': hour['weather'][0]['description'],
                    'icon': self.icon_url_template.format(icon=hour['weather'][0]['icon'])
                },
                'precipitation': hour.get('rain', {}).get('1h', 0) + hour.get('snow', {}).get('1h', 0)
            })
        
        # Daily forecast
        daily = []
        for day in data['daily'][:7]:  # Next 7 days
            daily.append({
                'date': datetime.fromtimestamp(day['dt']).strftime('%Y-%m-%d'),
                'day': datetime.fromtimestamp(day['dt']).strftime('%A'),
                'temp': {
                    'min': round(day['temp']['min']),
                    'max': round(day['temp']['max']),
                    'morning': round(day['temp']['morn']),
                    'day': round(day['temp']['day']),
                    'evening': round(day['temp']['eve']),
                    'night': round(day['temp']['night'])
                },
                'weather': {
                    'main': day['weather'][0]['main'],
                    'description': day['weather'][0]['description'],
                    'icon': self.icon_url_template.format(icon=day['weather'][0]['icon'])
                },
                'precipitation': {
                    'probability': day['pop'] * 100,  # Convert to percentage
                    'rain': day.get('rain', 0),
                    'snow': day.get('snow', 0)
                },
                'humidity': day['humidity'],
                'wind_speed': day['wind_speed'],
                'uvi': day['uvi']
            })
        
        return {
            'hourly': hourly,
            'daily': daily,
            'updated_at': datetime.now().isoformat()
        }
    
    def _format_geocode_results(self, data: List[Dict]) -> List[Dict]:
        """Format geocoding results."""
        results = []
        for item in data:
            results.append({
                'name': item['name'],
                'country': item['country'],
                'state': item.get('state', ''),
                'lat': item['lat'],
                'lon': item['lon'],
                'display_name': f"{item['name']}, {item.get('state', '')}, {item['country']}".strip(', ')
            })
        return results

Step 4: Create Web Routes

Implement the Flask routes for the web interface.

# app/routes.py
from flask import Blueprint, render_template, request, jsonify, flash, redirect, url_for, current_app
from flask_login import login_required, current_user
import json
from datetime import datetime
from app.weather_api import WeatherAPI
from app.models import FavoriteCity, SearchHistory, db
from app.forms import CitySearchForm, FavoriteCityForm
from app.utils import get_client_ip, validate_coordinates

main_bp = Blueprint('main', __name__)

@main_bp.route('/')
def index():
    """Home page with current location weather."""
    form = CitySearchForm()
    
    # Try to get user's location from IP or default
    lat, lon = None, None
    
    # Check if user has a default location
    if current_user.is_authenticated and current_user.default_location:
        # Geocode the default location
        weather_api = WeatherAPI()
        geocode_results = weather_api.geocode_city(current_user.default_location)
        if geocode_results:
            lat, lon = geocode_results[0]['lat'], geocode_results[0]['lon']
    
    # If no default location, try to get from request
    if not lat and request.args.get('lat') and request.args.get('lon'):
        try:
            lat = float(request.args.get('lat'))
            lon = float(request.args.get('lon'))
            if not validate_coordinates(lat, lon):
                lat, lon = None, None
        except ValueError:
            lat, lon = None, None
    
    weather_data = None
    forecast_data = None
    
    if lat and lon:
        weather_api = WeatherAPI()
        weather_data = weather_api.get_current_weather(lat, lon)
        forecast_data = weather_api.get_weather_forecast(lat, lon)
    
    return render_template('index.html', 
                         weather=weather_data, 
                         forecast=forecast_data,
                         form=form)

@main_bp.route('/city/<city_name>')
def city_weather(city_name):
    """Weather page for a specific city."""
    form = CitySearchForm()
    
    weather_api = WeatherAPI()
    geocode_results = weather_api.geocode_city(city_name)
    
    if not geocode_results:
        flash(f"City '{city_name}' not found.", 'error')
        return redirect(url_for('main.index'))
    
    # Use the first result
    city_data = geocode_results[0]
    lat, lon = city_data['lat'], city_data['lon']
    
    weather_data = weather_api.get_current_weather(lat, lon)
    forecast_data = weather_api.get_weather_forecast(lat, lon)
    
    # Log search history
    if current_user.is_authenticated:
        search_history = SearchHistory(
            query=city_name,
            result_count=len(geocode_results),
            user_id=current_user.id
        )
        db.session.add(search_history)
        db.session.commit()
    
    return render_template('city.html',
                         city=city_data,
                         weather=weather_data,
                         forecast=forecast_data,
                         form=form)

@main_bp.route('/search', methods=['POST'])
def search_city():
    """Search for cities."""
    form = CitySearchForm()
    
    if form.validate_on_submit():
        city_name = form.city.data
        
        weather_api = WeatherAPI()
        results = weather_api.geocode_city(city_name)
        
        if not results:
            flash(f"No cities found for '{city_name}'.", 'warning')
            return redirect(url_for('main.index'))
        
        if len(results) == 1:
            # Direct redirect if only one result
            return redirect(url_for('main.city_weather', city_name=city_name))
        else:
            # Show search results page (you could create this)
            return render_template('search_results.html', 
                                 query=city_name, 
                                 results=results,
                                 form=form)
    
    return redirect(url_for('main.index'))

@main_bp.route('/api/weather/<float:lat>/<float:lon>')
def api_weather(lat, lon):
    """API endpoint for weather data."""
    if not validate_coordinates(lat, lon):
        return jsonify({'error': 'Invalid coordinates'}), 400
    
    weather_api = WeatherAPI()
    weather_data = weather_api.get_current_weather(lat, lon)
    
    if weather_data:
        return jsonify(weather_data)
    else:
        return jsonify({'error': 'Weather data not available'}), 503

@main_bp.route('/api/forecast/<float:lat>/<float:lon>')
def api_forecast(lat, lon):
    """API endpoint for forecast data."""
    if not validate_coordinates(lat, lon):
        return jsonify({'error': 'Invalid coordinates'}), 400
    
    weather_api = WeatherAPI()
    forecast_data = weather_api.get_weather_forecast(lat, lon)
    
    if forecast_data:
        return jsonify(forecast_data)
    else:
        return jsonify({'error': 'Forecast data not available'}), 503

@main_bp.route('/favorites')
@login_required
def favorites():
    """User's favorite cities."""
    form = FavoriteCityForm()
    favorites = current_user.favorite_cities.all()
    
    # Get weather for favorite cities
    weather_api = WeatherAPI()
    favorites_weather = []
    
    for fav in favorites:
        weather = weather_api.get_current_weather(fav.latitude, fav.longitude)
        if weather:
            favorites_weather.append({
                'city': fav,
                'weather': weather
            })
    
    return render_template('favorites.html',
                         favorites=favorites_weather,
                         form=form)

@main_bp.route('/add_favorite', methods=['POST'])
@login_required
def add_favorite():
    """Add a city to favorites."""
    form = FavoriteCityForm()
    
    if form.validate_on_submit():
        city_name = form.city_name.data
        
        # Check if already in favorites
        existing = FavoriteCity.query.filter_by(
            user_id=current_user.id, 
            city_name=city_name
        ).first()
        
        if existing:
            flash(f"'{city_name}' is already in your favorites.", 'info')
            return redirect(url_for('main.favorites'))
        
        # Geocode the city
        weather_api = WeatherAPI()
        geocode_results = weather_api.geocode_city(city_name)
        
        if not geocode_results:
            flash(f"City '{city_name}' not found.", 'error')
            return redirect(url_for('main.favorites'))
        
        # Add to favorites
        city_data = geocode_results[0]
        favorite = FavoriteCity(
            city_name=city_data['name'],
            country_code=city_data['country'],
            latitude=city_data['lat'],
            longitude=city_data['lon'],
            user_id=current_user.id
        )
        
        db.session.add(favorite)
        db.session.commit()
        
        flash(f"'{city_name}' added to favorites!", 'success')
    
    return redirect(url_for('main.favorites'))

@main_bp.route('/remove_favorite/<int:favorite_id>', methods=['POST'])
@login_required
def remove_favorite(favorite_id):
    """Remove a city from favorites."""
    favorite = FavoriteCity.query.get_or_404(favorite_id)
    
    # Ensure user owns this favorite
    if favorite.user_id != current_user.id:
        flash("You don't have permission to remove this favorite.", 'error')
        return redirect(url_for('main.favorites'))
    
    db.session.delete(favorite)
    db.session.commit()
    
    flash(f"'{favorite.city_name}' removed from favorites.", 'success')
    return redirect(url_for('main.favorites'))

@main_bp.route('/settings', methods=['GET', 'POST'])
@login_required
def settings():
    """User settings page."""
    if request.method == 'POST':
        # Update user preferences
        temperature_unit = request.form.get('temperature_unit')
        theme = request.form.get('theme')
        default_location = request.form.get('default_location')
        
        if temperature_unit in ['celsius', 'fahrenheit']:
            current_user.temperature_unit = temperature_unit
        
        if theme in ['light', 'dark']:
            current_user.theme = theme
        
        current_user.default_location = default_location or None
        
        db.session.commit()
        flash('Settings updated successfully!', 'success')
    
    return render_template('settings.html')

@main_bp.errorhandler(404)
def page_not_found(e):
    """Handle 404 errors."""
    return render_template('error.html', error_code=404, error_message="Page not found"), 404

@main_bp.errorhandler(500)
def internal_error(e):
    """Handle 500 errors."""
    current_app.logger.error(f"Internal error: {e}")
    return render_template('error.html', error_code=500, error_message="Internal server error"), 500

Step 5: Create HTML Templates

Design the web interface with modern HTML and CSS.

<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}WeatherWise - Weather Dashboard{% endblock %}</title>
    
    <!-- Bootstrap CSS -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
    
    <!-- Font Awesome Icons -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
    
    <!-- Custom CSS -->
    <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
    
    {% block head %}{% endblock %}
</head>
<body class="{% if current_user.is_authenticated and current_user.theme == 'dark' %}dark-theme{% endif %}">
    <!-- Navigation -->
    <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
        <div class="container">
            <a class="navbar-brand" href="{{ url_for('main.index') }}">
                <i class="fas fa-cloud-sun"></i> WeatherWise
            </a>
            
            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
                <span class="navbar-toggler-icon"></span>
            </button>
            
            <div class="collapse navbar-collapse" id="navbarNav">
                <ul class="navbar-nav me-auto">
                    <li class="nav-item">
                        <a class="nav-link" href="{{ url_for('main.index') }}">
                            <i class="fas fa-home"></i> Home
                        </a>
                    </li>
                    {% if current_user.is_authenticated %}
                    <li class="nav-item">
                        <a class="nav-link" href="{{ url_for('main.favorites') }}">
                            <i class="fas fa-star"></i> Favorites
                        </a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="{{ url_for('main.settings') }}">
                            <i class="fas fa-cog"></i> Settings
                        </a>
                    </li>
                    {% endif %}
                </ul>
                
                <!-- Search Form -->
                <form class="d-flex" method="POST" action="{{ url_for('main.search_city') }}">
                    <input class="form-control me-2" type="search" name="city" placeholder="Search city..." required>
                    <button class="btn btn-outline-light" type="submit">
                        <i class="fas fa-search"></i>
                    </button>
                </form>
            </div>
        </div>
    </nav>

    <!-- Main Content -->
    <main class="container mt-4">
        {% with messages = get_flashed_messages(with_categories=true) %}
            {% if messages %}
                {% for category, message in messages %}
                    <div class="alert alert-{{ 'danger' if category == 'error' else 'success' if category == 'success' else 'warning' }} alert-dismissible fade show" role="alert">
                        {{ message }}
                        <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
                    </div>
                {% endfor %}
            {% endif %}
        {% endwith %}
        
        {% block content %}{% endblock %}
    </main>

    <!-- Footer -->
    <footer class="bg-light text-center text-muted mt-5 py-3">
        <div class="container">
            <p>&copy; 2024 WeatherWise. Built with Flask and OpenWeatherMap API.</p>
        </div>
    </footer>

    <!-- Bootstrap JS -->
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
    
    <!-- Chart.js for visualizations -->
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    
    <!-- Custom JavaScript -->
    <script src="{{ url_for('static', filename='js/app.js') }}"></script>
    
    {% block scripts %}{% endblock %}
</body>
</html>
<!-- templates/index.html -->
{% extends "base.html" %}

{% block title %}Weather Dashboard - {{ weather.location.name if weather else 'WeatherWise' }}{% endblock %}

{% block content %}
<div class="row">
    <!-- Current Weather -->
    <div class="col-lg-8">
        {% if weather %}
        <div class="card weather-card mb-4">
            <div class="card-header bg-primary text-white">
                <h5 class="card-title mb-0">
                    <i class="fas fa-map-marker-alt"></i> 
                    {{ weather.location.name }}, {{ weather.location.country }}
                </h5>
                <small>Last updated: {{ weather.updated_at | strftime('%H:%M') }}</small>
            </div>
            
            <div class="card-body">
                <div class="row">
                    <div class="col-md-6">
                        <div class="current-weather">
                            <img src="{{ weather.weather.icon }}" alt="{{ weather.weather.description }}" class="weather-icon">
                            <div class="temperature">
                                <span class="temp-main">{{ weather.temperature.current }}Β°C</span>
                                <span class="temp-feels">Feels like {{ weather.temperature.feels_like }}Β°C</span>
                            </div>
                            <div class="weather-desc">{{ weather.weather.description | title }}</div>
                        </div>
                    </div>
                    
                    <div class="col-md-6">
                        <div class="weather-details">
                            <div class="detail-row">
                                <span class="detail-label">High / Low:</span>
                                <span class="detail-value">{{ weather.temperature.max }}Β° / {{ weather.temperature.min }}Β°</span>
                            </div>
                            <div class="detail-row">
                                <span class="detail-label">Humidity:</span>
                                <span class="detail-value">{{ weather.details.humidity }}%</span>
                            </div>
                            <div class="detail-row">
                                <span class="detail-label">Wind:</span>
                                <span class="detail-value">{{ weather.details.wind_speed }} m/s</span>
                            </div>
                            <div class="detail-row">
                                <span class="detail-label">Pressure:</span>
                                <span class="detail-value">{{ weather.details.pressure }} hPa</span>
                            </div>
                            <div class="detail-row">
                                <span class="detail-label">Visibility:</span>
                                <span class="detail-value">{{ "%.1f"|format(weather.details.visibility) }} km</span>
                            </div>
                            <div class="detail-row">
                                <span class="detail-label">Sunrise / Sunset:</span>
                                <span class="detail-value">{{ weather.sun.sunrise }} / {{ weather.sun.sunset }}</span>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
        {% else %}
        <div class="card">
            <div class="card-body text-center">
                <i class="fas fa-cloud-sun fa-3x text-muted mb-3"></i>
                <h5>Weather data not available</h5>
                <p>Please search for a city or allow location access.</p>
                <button class="btn btn-primary" onclick="getCurrentLocation()">
                    <i class="fas fa-location-arrow"></i> Use Current Location
                </button>
            </div>
        </div>
        {% endif %}
    </div>
    
    <!-- Sidebar -->
    <div class="col-lg-4">
        <!-- Search Card -->
        <div class="card mb-4">
            <div class="card-header">
                <h6 class="card-title mb-0"><i class="fas fa-search"></i> Search City</h6>
            </div>
            <div class="card-body">
                <form method="POST" action="{{ url_for('main.search_city') }}">
                    <div class="input-group">
                        <input type="text" class="form-control" name="city" placeholder="Enter city name..." required>
                        <button class="btn btn-primary" type="submit">
                            <i class="fas fa-search"></i>
                        </button>
                    </div>
                </form>
            </div>
        </div>
        
        {% if current_user.is_authenticated %}
        <!-- Quick Actions -->
        <div class="card mb-4">
            <div class="card-header">
                <h6 class="card-title mb-0"><i class="fas fa-star"></i> Quick Actions</h6>
            </div>
            <div class="card-body">
                <a href="{{ url_for('main.favorites') }}" class="btn btn-outline-primary btn-sm mb-2">
                    <i class="fas fa-star"></i> View Favorites
                </a>
                <button class="btn btn-outline-secondary btn-sm" onclick="getCurrentLocation()">
                    <i class="fas fa-location-arrow"></i> Current Location
                </button>
            </div>
        </div>
        {% endif %}
        
        <!-- Weather Tips -->
        <div class="card">
            <div class="card-header">
                <h6 class="card-title mb-0"><i class="fas fa-info-circle"></i> Weather Tips</h6>
            </div>
            <div class="card-body">
                <ul class="list-unstyled small">
                    <li><i class="fas fa-umbrella text-primary"></i> Rain expected? Don't forget your umbrella!</li>
                    <li><i class="fas fa-sun text-warning"></i> UV Index high? Wear sunscreen and a hat.</li>
                    <li><i class="fas fa-wind text-info"></i> Windy day? Secure loose outdoor items.</li>
                </ul>
            </div>
        </div>
    </div>
</div>

<!-- Forecast Section -->
{% if forecast %}
<div class="row mt-4">
    <div class="col-12">
        <div class="card">
            <div class="card-header">
                <h5 class="card-title mb-0"><i class="fas fa-calendar-alt"></i> 7-Day Forecast</h5>
            </div>
            <div class="card-body">
                <div class="forecast-container">
                    {% for day in forecast.daily %}
                    <div class="forecast-day">
                        <div class="day-name">{{ day.day[:3] }}</div>
                        <img src="{{ day.weather.icon }}" alt="{{ day.weather.description }}" class="forecast-icon">
                        <div class="temp-high">{{ day.temp.max }}Β°</div>
                        <div class="temp-low">{{ day.temp.min }}Β°</div>
                        <div class="precipitation">{{ "%.0f"|format(day.precipitation.probability) }}%</div>
                    </div>
                    {% endfor %}
                </div>
            </div>
        </div>
    </div>
</div>
{% endif %}
{% endblock %}

{% block scripts %}
<script src="{{ url_for('static', filename='js/weather.js') }}"></script>
{% endblock %}

Step 6: Add CSS Styling

Create beautiful, responsive styles for the weather dashboard.

/* static/css/style.css */

/* Weather Card Styles */
.weather-card {
    border: none;
    border-radius: 15px;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
    overflow: hidden;
}

.weather-card .card-header {
    border-radius: 15px 15px 0 0 !important;
}

.current-weather {
    text-align: center;
    padding: 20px 0;
}

.weather-icon {
    width: 80px;
    height: 80px;
    margin-bottom: 10px;
}

.temperature .temp-main {
    font-size: 3rem;
    font-weight: 300;
    color: #333;
}

.temperature .temp-feels {
    font-size: 0.9rem;
    color: #666;
    margin-top: -5px;
}

.weather-desc {
    font-size: 1.1rem;
    color: #555;
    margin-top: 5px;
}

.weather-details {
    padding-left: 20px;
}

.detail-row {
    display: flex;
    justify-content: space-between;
    padding: 8px 0;
    border-bottom: 1px solid #f0f0f0;
}

.detail-row:last-child {
    border-bottom: none;
}

.detail-label {
    font-weight: 500;
    color: #666;
}

.detail-value {
    font-weight: 600;
    color: #333;
}

/* Forecast Styles */
.forecast-container {
    display: flex;
    justify-content: space-between;
    overflow-x: auto;
    padding: 10px 0;
}

.forecast-day {
    text-align: center;
    min-width: 100px;
    padding: 10px;
    border-radius: 10px;
    background: #f8f9fa;
    margin: 0 5px;
}

.forecast-icon {
    width: 40px;
    height: 40px;
    margin: 5px 0;
}

.temp-high {
    font-weight: 600;
    color: #d9534f;
}

.temp-low {
    color: #5bc0de;
    margin-top: -5px;
}

.day-name {
    font-weight: 600;
    color: #333;
    margin-bottom: 5px;
}

.precipitation {
    font-size: 0.8rem;
    color: #666;
    margin-top: 5px;
}

/* Dark Theme */
.dark-theme {
    background-color: #1a1a1a;
    color: #ffffff;
}

.dark-theme .navbar {
    background-color: #2c3e50 !important;
}

.dark-theme .card {
    background-color: #2c3e50;
    color: #ffffff;
    border-color: #34495e;
}

.dark-theme .forecast-day {
    background: #34495e;
    color: #ffffff;
}

.dark-theme .detail-row {
    border-bottom-color: #34495e;
}

/* Responsive Design */
@media (max-width: 768px) {
    .current-weather {
        margin-bottom: 20px;
    }
    
    .weather-details {
        padding-left: 0;
        text-align: center;
    }
    
    .forecast-container {
        padding-bottom: 20px;
    }
    
    .forecast-day {
        min-width: 80px;
        padding: 8px;
    }
}

/* Loading Animation */
.loading {
    opacity: 0.6;
    pointer-events: none;
}

.spinner {
    display: inline-block;
    width: 20px;
    height: 20px;
    border: 3px solid #f3f3f3;
    border-top: 3px solid #3498db;
    border-radius: 50%;
    animation: spin 1s linear infinite;
}

@keyframes spin {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
}

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

.weather-card {
    animation: fadeIn 0.5s ease-out;
}

.forecast-day {
    transition: transform 0.2s ease;
}

.forecast-day:hover {
    transform: translateY(-5px);
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}

Step 7: Add JavaScript Functionality

Create interactive features with JavaScript.

// static/js/weather.js

// Get current location weather
function getCurrentLocation() {
    if (navigator.geolocation) {
        navigator.geolocation.getCurrentPosition(
            function(position) {
                const lat = position.coords.latitude;
                const lon = position.coords.longitude;
                
                // Redirect to weather page with coordinates
                window.location.href = `/?lat=${lat}&lon=${lon}`;
            },
            function(error) {
                console.error('Geolocation error:', error);
                alert('Unable to get your location. Please search for a city instead.');
            }
        );
    } else {
        alert('Geolocation is not supported by this browser.');
    }
}

// Auto-refresh weather data
let autoRefreshInterval;

function startAutoRefresh() {
    // Refresh every 10 minutes
    autoRefreshInterval = setInterval(function() {
        if (document.visibilityState === 'visible') {
            location.reload();
        }
    }, 10 * 60 * 1000);
}

function stopAutoRefresh() {
    if (autoRefreshInterval) {
        clearInterval(autoRefreshInterval);
    }
}

// Start auto-refresh when page loads
document.addEventListener('DOMContentLoaded', function() {
    startAutoRefresh();
    
    // Stop auto-refresh when page is not visible
    document.addEventListener('visibilitychange', function() {
        if (document.visibilityState === 'hidden') {
            stopAutoRefresh();
        } else {
            startAutoRefresh();
        }
    });
});

// Temperature unit conversion
function convertTemperature(temp, fromUnit, toUnit) {
    if (fromUnit === toUnit) return temp;
    
    if (fromUnit === 'celsius' && toUnit === 'fahrenheit') {
        return (temp * 9/5) + 32;
    } else if (fromUnit === 'fahrenheit' && toUnit === 'celsius') {
        return (temp - 32) * 5/9;
    }
    
    return temp;
}

// Weather data caching
const weatherCache = new Map();

function cacheWeatherData(key, data, ttl = 10 * 60 * 1000) { // 10 minutes default
    const expiry = Date.now() + ttl;
    weatherCache.set(key, { data, expiry });
}

function getCachedWeatherData(key) {
    const cached = weatherCache.get(key);
    if (cached && cached.expiry > Date.now()) {
        return cached.data;
    }
    weatherCache.delete(key);
    return null;
}

// Error handling
function showWeatherError(message) {
    const errorDiv = document.createElement('div');
    errorDiv.className = 'alert alert-danger alert-dismissible fade show';
    errorDiv.innerHTML = `
        <i class="fas fa-exclamation-triangle"></i> ${message}
        <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
    `;
    
    const container = document.querySelector('.container');
    container.insertBefore(errorDiv, container.firstChild);
    
    // Auto-dismiss after 5 seconds
    setTimeout(() => {
        errorDiv.remove();
    }, 5000);
}

// Loading states
function setLoading(element, loading) {
    if (loading) {
        element.classList.add('loading');
        const spinner = document.createElement('div');
        spinner.className = 'spinner';
        element.appendChild(spinner);
    } else {
        element.classList.remove('loading');
        const spinner = element.querySelector('.spinner');
        if (spinner) {
            spinner.remove();
        }
    }
}

// Search suggestions (autocomplete)
let searchTimeout;
function setupSearchSuggestions() {
    const searchInput = document.querySelector('input[name="city"]');
    if (!searchInput) return;
    
    searchInput.addEventListener('input', function() {
        clearTimeout(searchTimeout);
        const query = this.value.trim();
        
        if (query.length < 2) return;
        
        searchTimeout = setTimeout(() => {
            fetch(`/api/geocode/${encodeURIComponent(query)}`)
                .then(response => response.json())
                .then(data => {
                    showSearchSuggestions(data);
                })
                .catch(error => {
                    console.error('Search suggestion error:', error);
                });
        }, 300);
    });
}

function showSearchSuggestions(suggestions) {
    // Remove existing suggestions
    const existing = document.querySelector('.search-suggestions');
    if (existing) existing.remove();
    
    if (!suggestions || suggestions.length === 0) return;
    
    const suggestionsDiv = document.createElement('div');
    suggestionsDiv.className = 'search-suggestions list-group position-absolute w-100';
    suggestionsDiv.style.zIndex = '1000';
    
    suggestions.forEach(suggestion => {
        const item = document.createElement('button');
        item.className = 'list-group-item list-group-item-action';
        item.textContent = suggestion.display_name;
        item.addEventListener('click', () => {
            document.querySelector('input[name="city"]').value = suggestion.name;
            suggestionsDiv.remove();
            // Submit form
            item.closest('form').submit();
        });
        suggestionsDiv.appendChild(item);
    });
    
    // Position suggestions
    const input = document.querySelector('input[name="city"]');
    input.parentNode.style.position = 'relative';
    input.parentNode.appendChild(suggestionsDiv);
    
    // Hide suggestions when clicking outside
    document.addEventListener('click', function hideSuggestions(e) {
        if (!suggestionsDiv.contains(e.target) && e.target !== input) {
            suggestionsDiv.remove();
            document.removeEventListener('click', hideSuggestions);
        }
    });
}

// Initialize features when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
    setupSearchSuggestions();
    
    // Add loading states to forms
    document.querySelectorAll('form').forEach(form => {
        form.addEventListener('submit', function() {
            const submitBtn = form.querySelector('button[type="submit"]');
            if (submitBtn) {
                submitBtn.disabled = true;
                submitBtn.innerHTML = '<span class="spinner"></span> Loading...';
            }
        });
    });
});

Step 8: Create Requirements and Setup

# requirements.txt
Flask==2.3.3
Flask-SQLAlchemy==3.0.5
Flask-Caching==2.1.0
requests==2.31.0
python-dotenv==1.0.0
Werkzeug==2.3.7
Jinja2==3.1.2
Click==8.1.7
# .env.example
SECRET_KEY=your-secret-key-here
WEATHER_API_KEY=your-openweathermap-api-key-here
FLASK_ENV=development
DATABASE_URL=sqlite:///weatherwise.db

Step 9: Testing the Application

Create comprehensive tests for the weather dashboard.

# tests/test_weather_api.py
import unittest
from unittest.mock import patch, MagicMock
from app.weather_api import WeatherAPI

class TestWeatherAPI(unittest.TestCase):
    """Test cases for WeatherAPI class."""
    
    def setUp(self):
        """Set up test fixtures."""
        self.api = WeatherAPI()
    
    @patch('app.weather_api.requests.get')
    def test_get_current_weather_success(self, mock_get):
        """Test successful current weather retrieval."""
        # Mock response
        mock_response = MagicMock()
        mock_response.raise_for_status.return_value = None
        mock_response.json.return_value = {
            'name': 'London',
            'sys': {'country': 'GB'},
            'coord': {'lat': 51.5074, 'lon': -0.1278},
            'weather': [{'main': 'Clouds', 'description': 'overcast clouds', 'icon': '04d'}],
            'main': {
                'temp': 15.5, 'feels_like': 14.8, 'temp_min': 13.0, 'temp_max': 18.0,
                'humidity': 72, 'pressure': 1013
            },
            'wind': {'speed': 3.5, 'deg': 180},
            'visibility': 10000,
            'clouds': {'all': 90},
            'sys': {'sunrise': 1609459200, 'sunset': 1609488000}
        }
        mock_get.return_value = mock_response
        
        result = self.api.get_current_weather(51.5074, -0.1278)
        
        self.assertIsNotNone(result)
        self.assertEqual(result['location']['name'], 'London')
        self.assertEqual(result['temperature']['current'], 16)  # Rounded
        self.assertEqual(result['weather']['main'], 'Clouds')
    
    @patch('app.weather_api.requests.get')
    def test_get_current_weather_api_error(self, mock_get):
        """Test handling of API errors."""
        mock_get.side_effect = Exception("API Error")
        
        result = self.api.get_current_weather(51.5074, -0.1278)
        
        self.assertIsNone(result)
    
    def test_format_current_weather(self):
        """Test weather data formatting."""
        raw_data = {
            'name': 'Paris',
            'sys': {'country': 'FR'},
            'coord': {'lat': 48.8566, 'lon': 2.3522},
            'weather': [{'main': 'Clear', 'description': 'clear sky', 'icon': '01d'}],
            'main': {'temp': 20.0, 'feels_like': 19.5, 'temp_min': 18.0, 'temp_max': 22.0, 'humidity': 65, 'pressure': 1015},
            'wind': {'speed': 2.1, 'deg': 90},
            'visibility': 10000,
            'clouds': {'all': 20},
            'sys': {'sunrise': 1609459200, 'sunset': 1609488000}
        }
        
        result = self.api._format_current_weather(raw_data)
        
        self.assertEqual(result['location']['name'], 'Paris')
        self.assertEqual(result['temperature']['current'], 20)
        self.assertEqual(result['weather']['main'], 'Clear')
        self.assertEqual(result['details']['humidity'], 65)

if __name__ == '__main__':
    unittest.main()

Step 10: Deployment Preparation

Create deployment configurations for production.

# wsgi.py
from app import create_app

app = create_app('production')

if __name__ == "__main__":
    app.run()
# Procfile (for Heroku deployment)
web: gunicorn wsgi:app
# Dockerfile
FROM python:3.9-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 5000

CMD ["python", "run.py"]

Usage Examples

Basic Usage

# Install dependencies
pip install -r requirements.txt

# Set environment variables
export WEATHER_API_KEY=your-api-key-here
export SECRET_KEY=your-secret-key-here

# Initialize database
flask init-db

# Run the application
python run.py

API Endpoints

# Get current weather
GET /api/weather/51.5074/-0.1278

# Get forecast
GET /api/forecast/51.5074/-0.1278

# Search cities
POST /search (form data: city="London")

User Features

# Add favorite city
POST /add_favorite (form data: city_name="London")

# View favorites
GET /favorites

# Update settings
POST /settings (form data: temperature_unit="fahrenheit", theme="dark")

Advanced Features to Consider

  1. Weather Alerts - Severe weather notifications
  2. Historical Data - Past weather trends
  3. Weather Maps - Interactive weather maps
  4. Multiple Locations - Compare weather across cities
  5. Weather Widgets - Embeddable weather widgets
  6. Mobile App - Companion mobile application
  7. Weather API - Public API for other developers

Summary

WeatherWise demonstrates modern web development:

Core Technologies:

  • Flask web framework with application factory pattern
  • SQLAlchemy ORM for database management
  • OpenWeatherMap API integration
  • Responsive HTML/CSS with Bootstrap
  • Interactive JavaScript features

Key Skills:

  • Full-stack web application development
  • RESTful API design and consumption
  • Database design and relationships
  • User authentication and sessions
  • Error handling and logging
  • Responsive web design
  • API rate limiting and caching

Production Features:

  • Environment-based configuration
  • Database migrations
  • Caching for performance
  • Error monitoring and logging
  • Security best practices

Next Steps:

  1. Obtain OpenWeatherMap API key
  2. Implement user authentication
  3. Add comprehensive error handling
  4. Deploy to cloud platform
  5. Add advanced weather features

Congratulations! You’ve built a complete web application! 🌦️

Ready for the next project? Let’s build a File Organizer! πŸ“