Skip to main content

🌀️ Module 4 Project: Weather Dashboard

Congratulations on completing Module 4! πŸŽ‰ Now it's time to put everything together and build a real-world application. You'll create a beautiful, functional weather dashboard that fetches real data from a weather API, caches results, handles errors gracefully, and provides an excellent user experience. This project combines useEffect, custom hooks, data fetching, API integration, error handling, and more. By the end, you'll have a portfolio-worthy project that demonstrates your mastery of React's data fetching ecosystem. Let's build something amazing! β˜€οΈ

🎯 Project Goals

By completing this project, you will:

  • Build a complete, real-world React application from scratch
  • Integrate with the OpenWeatherMap API
  • Implement city search with debouncing
  • Display current weather and 5-day forecast
  • Create custom hooks for weather data fetching
  • Implement client-side caching with localStorage
  • Handle loading states and errors gracefully
  • Build a responsive, mobile-friendly UI
  • Use TypeScript for type safety throughout
  • Apply all best practices from Module 4

Estimated Time: 3-4 hours

Difficulty: Intermediate

πŸ“‘ Project Guide

πŸ“‹ Project Overview

You're building a weather dashboard that shows current weather and forecasts for cities around the world. Users can search for cities, view detailed weather information, and save their favorite locations.

Features We'll Build

Core Features (Must Have)

  • πŸ” City Search - Search for cities with debounced input
  • 🌑️ Current Weather - Temperature, conditions, humidity, wind speed
  • πŸ“… 5-Day Forecast - Daily high/low temperatures and conditions
  • πŸ’Ύ Caching - Cache weather data to reduce API calls
  • ⚠️ Error Handling - Graceful error messages and retry options
  • ⏳ Loading States - Skeleton screens and spinners
  • πŸ“± Responsive Design - Mobile-friendly layout

Advanced Features (Should Have)

  • ⭐ Favorites - Save favorite cities to localStorage
  • πŸ”„ Auto-Refresh - Update weather data every 10 minutes
  • 🌍 Geolocation - Get weather for user's current location
  • 🎨 Dynamic Backgrounds - Change UI based on weather conditions
  • πŸ“Š Weather Charts - Visualize temperature trends

Bonus Features (Nice to Have)

  • πŸŒ“ Dark Mode - Toggle between light and dark themes
  • 🌑️ Unit Toggle - Switch between Celsius and Fahrenheit
  • 🌐 Multiple Languages - i18n support
  • πŸ“ Map View - Show city on a map
  • πŸ”” Weather Alerts - Notifications for severe weather

What You'll Practice

βœ… Module 4 Concepts

  • useEffect - Fetching data, cleanup, dependency arrays
  • Data Fetching - Async/await, loading states, error handling
  • Custom Hooks - useWeather, useFavorites, useDebounce
  • Advanced Patterns - Debouncing, caching, race conditions
  • API Integration - Service layer, TypeScript types, environment variables

Final Result Preview

Your completed weather dashboard will look something like this:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  🌀️ Weather Dashboard                      β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚  β”‚ πŸ” Search cities...                   β”‚ β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚                                             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚ πŸ“ San Francisco, CA                β”‚   β”‚
β”‚  β”‚                                     β”‚   β”‚
β”‚  β”‚    β˜€οΈ                              β”‚   β”‚
β”‚  β”‚    22Β°C                            β”‚   β”‚
β”‚  β”‚    Sunny                           β”‚   β”‚
β”‚  β”‚                                     β”‚   β”‚
β”‚  β”‚    πŸ’§ Humidity: 65%                β”‚   β”‚
β”‚  β”‚    πŸ’¨ Wind: 12 km/h                β”‚   β”‚
β”‚  β”‚    πŸ‘οΈ Visibility: 10 km            β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                                             β”‚
β”‚  πŸ“… 5-Day Forecast                          β”‚
β”‚  β”Œβ”€β”€β”€β”¬β”€β”€β”€β”¬β”€β”€β”€β”¬β”€β”€β”€β”¬β”€β”€β”€β”                     β”‚
β”‚  β”‚Monβ”‚Tueβ”‚Wedβ”‚Thuβ”‚Friβ”‚                     β”‚
β”‚  β”‚β˜€οΈ β”‚β›…β”‚πŸŒ§οΈβ”‚β›ˆοΈ β”‚β˜€οΈ β”‚                     β”‚
β”‚  β”‚24Β°β”‚22Β°β”‚19Β°β”‚18Β°β”‚23Β°β”‚                     β”‚
β”‚  β””β”€β”€β”€β”΄β”€β”€β”€β”΄β”€β”€β”€β”΄β”€β”€β”€β”΄β”€β”€β”€β”˜                     β”‚
β”‚                                             β”‚
β”‚  ⭐ Favorites: NYC, London, Tokyo          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Application Architecture

graph TD A[App Component] --> B[Search Bar] A --> C[Current Weather Display] A --> D[Forecast List] A --> E[Favorites Bar] B --> F[useDebounce Hook] B --> G[Weather Service] C --> H[useWeather Hook] D --> H H --> G H --> I[Cache Layer] E --> J[useFavorites Hook] J --> K[localStorage] G --> L[OpenWeatherMap API] style A fill:#667eea,stroke:#333,stroke-width:2px,color:#fff style H fill:#764ba2,stroke:#333,stroke-width:2px,color:#fff style G fill:#f093fb,stroke:#333,stroke-width:2px,color:#fff

πŸ’‘ Learning Approach

This project guide is structured to walk you through building the app step by step. Each section includes:

  • Clear objectives - What you'll build in this section
  • Code examples - Complete, working code you can use
  • Explanations - Why we're doing things this way
  • Challenges - Optional extensions to try on your own

Tip: Try to implement features yourself first, then check the provided solutions!

βš™οΈ Setting Up the Project

Let's create our project with Vite and set up the initial structure.

Step 1: Create the Project

# Create a new Vite project with React and TypeScript
npm create vite@latest weather-dashboard -- --template react-ts

# Navigate into the project
cd weather-dashboard

# Install dependencies
npm install

# Start the development server
npm run dev

Step 2: Install Additional Dependencies

# Optional: Install date formatting library
npm install date-fns

# Optional: Install icons (if you want to use React Icons)
npm install react-icons

Step 3: Clean Up Default Files

Remove the default Vite boilerplate:

  • Delete src/App.css
  • Delete src/index.css (we'll create our own)
  • Clear the content of src/App.tsx

Step 4: Create Basic Styles

/* src/index.css */
:root {
    --primary: #667eea;
    --primary-dark: #5a67d8;
    --secondary: #764ba2;
    --success: #48bb78;
    --danger: #f56565;
    --warning: #ed8936;
    --info: #4299e1;
    
    --bg-primary: #ffffff;
    --bg-secondary: #f7fafc;
    --text-primary: #2d3748;
    --text-secondary: #718096;
    --border: #e2e8f0;
    
    --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
    --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
    --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
    
    --radius: 8px;
    --spacing: 1rem;
}

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

body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 
                 'Helvetica Neue', Arial, sans-serif;
    background: var(--bg-secondary);
    color: var(--text-primary);
    line-height: 1.6;
}

.container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 2rem 1rem;
}

.card {
    background: var(--bg-primary);
    border-radius: var(--radius);
    padding: 1.5rem;
    box-shadow: var(--shadow-md);
    margin-bottom: 1rem;
}

.btn {
    padding: 0.75rem 1.5rem;
    border: none;
    border-radius: var(--radius);
    font-size: 1rem;
    cursor: pointer;
    transition: all 0.2s;
}

.btn-primary {
    background: var(--primary);
    color: white;
}

.btn-primary:hover {
    background: var(--primary-dark);
}

.input {
    width: 100%;
    padding: 0.75rem;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    font-size: 1rem;
}

.input:focus {
    outline: none;
    border-color: var(--primary);
    box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}

/* Loading spinner */
.spinner {
    border: 3px solid var(--border);
    border-top: 3px solid var(--primary);
    border-radius: 50%;
    width: 40px;
    height: 40px;
    animation: spin 1s linear infinite;
    margin: 2rem auto;
}

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

/* Error message */
.error {
    background: #fff5f5;
    border: 1px solid var(--danger);
    color: var(--danger);
    padding: 1rem;
    border-radius: var(--radius);
    margin: 1rem 0;
}

/* Responsive */
@media (max-width: 768px) {
    .container {
        padding: 1rem;
    }
    
    .card {
        padding: 1rem;
    }
}

Step 5: Basic App Structure

// src/App.tsx
import { useState } from 'react';
import './index.css';

function App() {
    return (
        <div className="container">
            <header>
                <h1 style={{ 
                    textAlign: 'center', 
                    margin: '2rem 0',
                    background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
                    WebkitBackgroundClip: 'text',
                    WebkitTextFillColor: 'transparent',
                    fontSize: '2.5rem'
                }}>
                    🌀️ Weather Dashboard
                </h1>
            </header>
            
            <main>
                <div className="card">
                    <p>Weather dashboard coming soon...</p>
                </div>
            </main>
        </div>
    );
}

export default App;

βœ… Checkpoint

At this point, you should have:

  • βœ… A working Vite + React + TypeScript project
  • βœ… Basic styles defined in index.css
  • βœ… A clean App.tsx with a header
  • βœ… The development server running at localhost:5173

You should see "Weather Dashboard" heading in your browser! πŸŽ‰

πŸ”‘ Getting an API Key

We'll use the OpenWeatherMap API, which is free and perfect for learning. Let's get your API key!

Step 1: Sign Up for OpenWeatherMap

  1. Go to OpenWeatherMap.org
  2. Click "Sign In" β†’ "Create an Account"
  3. Fill in your details and verify your email
  4. Go to "API keys" tab in your account
  5. Copy your default API key (or generate a new one)

Note: It can take up to 2 hours for your API key to activate, but usually it's instant.

Step 2: Set Up Environment Variables

Create a .env file in your project root:

# .env
VITE_WEATHER_API_KEY=your_api_key_here
VITE_WEATHER_API_URL=https://api.openweathermap.org/data/2.5

Create a .env.example for your repository:

# .env.example
VITE_WEATHER_API_KEY=
VITE_WEATHER_API_URL=https://api.openweathermap.org/data/2.5

Add .env to your .gitignore:

# .gitignore
.env
.env.local
.env.*.local

Step 3: Create Configuration File

// src/config/env.ts
export const config = {
    weatherApiKey: import.meta.env.VITE_WEATHER_API_KEY,
    weatherApiUrl: import.meta.env.VITE_WEATHER_API_URL || 'https://api.openweathermap.org/data/2.5',
    isDevelopment: import.meta.env.DEV,
    isProduction: import.meta.env.PROD,
} as const;

// Validate required environment variables
if (!config.weatherApiKey) {
    throw new Error('VITE_WEATHER_API_KEY is required in .env file');
}

API Endpoints We'll Use

Endpoint Purpose Example
/weather Current weather /weather?q=London&appid=KEY
/forecast 5-day forecast /forecast?q=London&appid=KEY
/weather By coordinates /weather?lat=51.5&lon=-0.1&appid=KEY

⚠️ Important Notes

  • Free tier limits: 60 calls/minute, 1,000,000 calls/month
  • Units: Add &units=metric for Celsius, &units=imperial for Fahrenheit
  • Default: Temperature is in Kelvin if no units specified
  • API key: Never commit your actual API key to version control!

βœ… Checkpoint

At this point, you should have:

  • βœ… An OpenWeatherMap account with an API key
  • βœ… .env file with your API key (not committed to git)
  • βœ… .env.example file (safe to commit)
  • βœ… config/env.ts file that validates the API key

Ready to start making API calls! πŸš€

πŸ“ Project Structure

Let's organize our project with a clean, scalable structure. Here's how we'll organize our files:

weather-dashboard/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ components/
β”‚   β”‚   β”œβ”€β”€ SearchBar.tsx
β”‚   β”‚   β”œβ”€β”€ CurrentWeather.tsx
β”‚   β”‚   β”œβ”€β”€ ForecastCard.tsx
β”‚   β”‚   β”œβ”€β”€ ForecastList.tsx
β”‚   β”‚   β”œβ”€β”€ FavoritesList.tsx
β”‚   β”‚   β”œβ”€β”€ ErrorDisplay.tsx
β”‚   β”‚   └── LoadingSpinner.tsx
β”‚   β”œβ”€β”€ hooks/
β”‚   β”‚   β”œβ”€β”€ useWeather.ts
β”‚   β”‚   β”œβ”€β”€ useDebounce.ts
β”‚   β”‚   β”œβ”€β”€ useFavorites.ts
β”‚   β”‚   └── useGeolocation.ts
β”‚   β”œβ”€β”€ services/
β”‚   β”‚   β”œβ”€β”€ weatherService.ts
β”‚   β”‚   └── cacheService.ts
β”‚   β”œβ”€β”€ types/
β”‚   β”‚   β”œβ”€β”€ weather.ts
β”‚   β”‚   └── index.ts
β”‚   β”œβ”€β”€ utils/
β”‚   β”‚   β”œβ”€β”€ formatters.ts
β”‚   β”‚   └── weatherIcons.ts
β”‚   β”œβ”€β”€ config/
β”‚   β”‚   └── env.ts
β”‚   β”œβ”€β”€ App.tsx
β”‚   β”œβ”€β”€ main.tsx
β”‚   └── index.css
β”œβ”€β”€ .env
β”œβ”€β”€ .env.example
β”œβ”€β”€ .gitignore
β”œβ”€β”€ package.json
β”œβ”€β”€ tsconfig.json
└── vite.config.ts

Create the Folder Structure

# From your project root, create all folders
mkdir -p src/components src/hooks src/services src/types src/utils src/config

πŸ’‘ Organization Philosophy

  • components/ - Reusable UI components
  • hooks/ - Custom hooks for logic reuse
  • services/ - API calls and business logic
  • types/ - TypeScript type definitions
  • utils/ - Helper functions
  • config/ - Configuration and constants

πŸ”Œ Building the API Service

Let's create our weather service that handles all API communication. We'll start with TypeScript types, then build the service layer.

Step 1: Define Weather Types

// src/types/weather.ts

// Current weather data from API
export interface WeatherData {
    coord: {
        lon: number;
        lat: number;
    };
    weather: Array<{
        id: number;
        main: string;
        description: string;
        icon: string;
    }>;
    main: {
        temp: number;
        feels_like: number;
        temp_min: number;
        temp_max: number;
        pressure: number;
        humidity: number;
    };
    wind: {
        speed: number;
        deg: number;
    };
    clouds: {
        all: number;
    };
    dt: number;
    sys: {
        country: string;
        sunrise: number;
        sunset: number;
    };
    name: string;
    visibility?: number;
}

// Forecast data from API
export interface ForecastData {
    list: Array<{
        dt: number;
        main: {
            temp: number;
            feels_like: number;
            temp_min: number;
            temp_max: number;
            humidity: number;
        };
        weather: Array<{
            id: number;
            main: string;
            description: string;
            icon: string;
        }>;
        wind: {
            speed: number;
        };
        dt_txt: string;
    }>;
    city: {
        name: string;
        country: string;
        sunrise: number;
        sunset: number;
    };
}

// Processed daily forecast
export interface DailyForecast {
    date: string;
    temp: number;
    tempMin: number;
    tempMax: number;
    description: string;
    icon: string;
    humidity: number;
    windSpeed: number;
}

// City search result
export interface City {
    name: string;
    country: string;
    lat: number;
    lon: number;
}

Step 2: Create Cache Service

// src/services/cacheService.ts

interface CacheEntry<T> {
    data: T;
    timestamp: number;
}

class CacheService {
    private cache = new Map<string, CacheEntry<any>>();
    private defaultTTL = 10 * 60 * 1000; // 10 minutes

    set<T>(key: string, data: T, ttl?: number): void {
        this.cache.set(key, {
            data,
            timestamp: Date.now()
        });
    }

    get<T>(key: string, ttl?: number): T | null {
        const entry = this.cache.get(key);
        
        if (!entry) return null;

        const age = Date.now() - entry.timestamp;
        const maxAge = ttl || this.defaultTTL;

        if (age > maxAge) {
            this.cache.delete(key);
            return null;
        }

        return entry.data;
    }

    has(key: string, ttl?: number): boolean {
        return this.get(key, ttl) !== null;
    }

    clear(): void {
        this.cache.clear();
    }

    delete(key: string): void {
        this.cache.delete(key);
    }
}

export const cacheService = new CacheService();

Step 3: Create Weather Service

βœ… Complete Weather Service

// src/services/weatherService.ts
import { config } from '@/config/env';
import { cacheService } from './cacheService';
import { WeatherData, ForecastData, DailyForecast } from '@/types/weather';

class WeatherService {
    private baseUrl = config.weatherApiUrl;
    private apiKey = config.weatherApiKey;

    private buildUrl(endpoint: string, params: Record<string, string>): string {
        const searchParams = new URLSearchParams({
            ...params,
            appid: this.apiKey,
            units: 'metric' // Use Celsius
        });

        return `${this.baseUrl}${endpoint}?${searchParams.toString()}`;
    }

    async getCurrentWeather(city: string): Promise<WeatherData> {
        const cacheKey = `weather-${city.toLowerCase()}`;
        
        // Check cache first
        const cached = cacheService.get<WeatherData>(cacheKey);
        if (cached) {
            console.log('Returning cached weather data for:', city);
            return cached;
        }

        // Fetch from API
        const url = this.buildUrl('/weather', { q: city });
        
        try {
            const response = await fetch(url);
            
            if (!response.ok) {
                if (response.status === 404) {
                    throw new Error(`City "${city}" not found`);
                }
                throw new Error(`Failed to fetch weather data: ${response.statusText}`);
            }

            const data: WeatherData = await response.json();
            
            // Cache the result
            cacheService.set(cacheKey, data);
            
            return data;
        } catch (error) {
            if (error instanceof Error) {
                throw error;
            }
            throw new Error('An unexpected error occurred');
        }
    }

    async getWeatherByCoords(lat: number, lon: number): Promise<WeatherData> {
        const cacheKey = `weather-coords-${lat}-${lon}`;
        
        // Check cache first
        const cached = cacheService.get<WeatherData>(cacheKey);
        if (cached) {
            console.log('Returning cached weather data for coordinates');
            return cached;
        }

        // Fetch from API
        const url = this.buildUrl('/weather', {
            lat: lat.toString(),
            lon: lon.toString()
        });
        
        try {
            const response = await fetch(url);
            
            if (!response.ok) {
                throw new Error(`Failed to fetch weather data: ${response.statusText}`);
            }

            const data: WeatherData = await response.json();
            
            // Cache the result
            cacheService.set(cacheKey, data);
            
            return data;
        } catch (error) {
            if (error instanceof Error) {
                throw error;
            }
            throw new Error('An unexpected error occurred');
        }
    }

    async getForecast(city: string): Promise<DailyForecast[]> {
        const cacheKey = `forecast-${city.toLowerCase()}`;
        
        // Check cache first
        const cached = cacheService.get<DailyForecast[]>(cacheKey);
        if (cached) {
            console.log('Returning cached forecast data for:', city);
            return cached;
        }

        // Fetch from API
        const url = this.buildUrl('/forecast', { q: city });
        
        try {
            const response = await fetch(url);
            
            if (!response.ok) {
                if (response.status === 404) {
                    throw new Error(`City "${city}" not found`);
                }
                throw new Error(`Failed to fetch forecast data: ${response.statusText}`);
            }

            const data: ForecastData = await response.json();
            
            // Process forecast data - group by day and get daily high/low
            const dailyForecasts = this.processForecastData(data);
            
            // Cache the result
            cacheService.set(cacheKey, dailyForecasts);
            
            return dailyForecasts;
        } catch (error) {
            if (error instanceof Error) {
                throw error;
            }
            throw new Error('An unexpected error occurred');
        }
    }

    private processForecastData(data: ForecastData): DailyForecast[] {
        // Group forecast data by day
        const dailyData = new Map<string, typeof data.list[0][]>();

        data.list.forEach(item => {
            const date = item.dt_txt.split(' ')[0]; // Get date part (YYYY-MM-DD)
            
            if (!dailyData.has(date)) {
                dailyData.set(date, []);
            }
            
            dailyData.get(date)!.push(item);
        });

        // Convert to DailyForecast array (take first 5 days)
        const forecasts: DailyForecast[] = [];
        
        Array.from(dailyData.entries()).slice(0, 5).forEach(([date, items]) => {
            // Calculate daily averages and extremes
            const temps = items.map(item => item.main.temp);
            const tempMin = Math.min(...temps);
            const tempMax = Math.max(...temps);
            const avgTemp = temps.reduce((a, b) => a + b) / temps.length;
            
            // Use midday weather data (around 12:00) if available
            const middayItem = items.find(item => item.dt_txt.includes('12:00:00')) || items[0];

            forecasts.push({
                date,
                temp: Math.round(avgTemp),
                tempMin: Math.round(tempMin),
                tempMax: Math.round(tempMax),
                description: middayItem.weather[0].description,
                icon: middayItem.weather[0].icon,
                humidity: middayItem.main.humidity,
                windSpeed: middayItem.wind.speed
            });
        });

        return forecasts;
    }
}

export const weatherService = new WeatherService();

πŸ’‘ Service Layer Benefits

  • Centralized API logic - All weather calls in one place
  • Built-in caching - Reduces API calls and improves performance
  • Error handling - Consistent error messages
  • Type safety - Full TypeScript support
  • Testability - Easy to mock for testing

Step 4: Create Utility Functions

// src/utils/formatters.ts
import { format } from 'date-fns';

export function formatTemperature(temp: number): string {
    return `${Math.round(temp)}Β°C`;
}

export function formatDate(dateString: string): string {
    const date = new Date(dateString);
    return format(date, 'EEE, MMM d');
}

export function formatTime(timestamp: number): string {
    return format(new Date(timestamp * 1000), 'h:mm a');
}

export function capitalizeWords(str: string): string {
    return str
        .split(' ')
        .map(word => word.charAt(0).toUpperCase() + word.slice(1))
        .join(' ');
}
// src/utils/weatherIcons.ts

// Map OpenWeatherMap icon codes to emojis
export function getWeatherEmoji(iconCode: string): string {
    const iconMap: Record<string, string> = {
        '01d': 'β˜€οΈ', // clear sky day
        '01n': 'πŸŒ™', // clear sky night
        '02d': 'β›…', // few clouds day
        '02n': '☁️', // few clouds night
        '03d': '☁️', // scattered clouds
        '03n': '☁️',
        '04d': '☁️', // broken clouds
        '04n': '☁️',
        '09d': '🌧️', // shower rain
        '09n': '🌧️',
        '10d': '🌦️', // rain day
        '10n': '🌧️', // rain night
        '11d': 'β›ˆοΈ', // thunderstorm
        '11n': 'β›ˆοΈ',
        '13d': '❄️', // snow
        '13n': '❄️',
        '50d': '🌫️', // mist
        '50n': '🌫️'
    };

    return iconMap[iconCode] || '🌀️';
}

export function getWeatherBackground(iconCode: string): string {
    const first2 = iconCode.slice(0, 2);
    
    const backgroundMap: Record<string, string> = {
        '01': 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', // clear
        '02': 'linear-gradient(135deg, #89f7fe 0%, #66a6ff 100%)', // few clouds
        '03': 'linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)', // scattered clouds
        '04': 'linear-gradient(135deg, #d299c2 0%, #fef9d7 100%)', // broken clouds
        '09': 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)', // shower rain
        '10': 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)', // rain
        '11': 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)', // thunderstorm
        '13': 'linear-gradient(135deg, #e0c3fc 0%, #8ec5fc 100%)', // snow
        '50': 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)'  // mist
    };

    return backgroundMap[first2] || 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)';
}

βœ… Checkpoint

At this point, you should have:

  • βœ… Complete type definitions for weather data
  • βœ… Cache service for storing API responses
  • βœ… Weather service with getCurrentWeather, getWeatherByCoords, and getForecast methods
  • βœ… Utility functions for formatting and icons

Your API layer is ready! Time to build custom hooks. 🎣

🎣 Creating Custom Hooks

Let's create reusable custom hooks that encapsulate our app's logic.

Hook 1: useDebounce

// src/hooks/useDebounce.ts
import { useState, useEffect } from 'react';

export function useDebounce<T>(value: T, delay: number): T {
    const [debouncedValue, setDebouncedValue] = useState<T>(value);

    useEffect(() => {
        // Set up a timer to update the debounced value
        const timer = setTimeout(() => {
            setDebouncedValue(value);
        }, delay);

        // Clean up the timer if value changes before delay expires
        return () => {
            clearTimeout(timer);
        };
    }, [value, delay]);

    return debouncedValue;
}

Hook 2: useWeather

βœ… Complete useWeather Hook

// src/hooks/useWeather.ts
import { useState, useEffect } from 'react';
import { weatherService } from '@/services/weatherService';
import { WeatherData, DailyForecast } from '@/types/weather';

interface UseWeatherReturn {
    weather: WeatherData | null;
    forecast: DailyForecast[];
    loading: boolean;
    error: string | null;
    fetchWeather: (city: string) => Promise<void>;
    fetchWeatherByCoords: (lat: number, lon: number) => Promise<void>;
}

export function useWeather(): UseWeatherReturn {
    const [weather, setWeather] = useState<WeatherData | null>(null);
    const [forecast, setForecast] = useState<DailyForecast[]>([]);
    const [loading, setLoading] = useState(false);
    const [error, setError] = useState<string | null>(null);

    const fetchWeather = async (city: string) => {
        if (!city.trim()) return;

        setLoading(true);
        setError(null);

        try {
            const [weatherData, forecastData] = await Promise.all([
                weatherService.getCurrentWeather(city),
                weatherService.getForecast(city)
            ]);

            setWeather(weatherData);
            setForecast(forecastData);
        } catch (err) {
            setError(err instanceof Error ? err.message : 'Failed to fetch weather data');
            setWeather(null);
            setForecast([]);
        } finally {
            setLoading(false);
        }
    };

    const fetchWeatherByCoords = async (lat: number, lon: number) => {
        setLoading(true);
        setError(null);

        try {
            const weatherData = await weatherService.getWeatherByCoords(lat, lon);
            setWeather(weatherData);

            // Fetch forecast using city name from weather data
            const forecastData = await weatherService.getForecast(weatherData.name);
            setForecast(forecastData);
        } catch (err) {
            setError(err instanceof Error ? err.message : 'Failed to fetch weather data');
            setWeather(null);
            setForecast([]);
        } finally {
            setLoading(false);
        }
    };

    return {
        weather,
        forecast,
        loading,
        error,
        fetchWeather,
        fetchWeatherByCoords
    };
}

Hook 3: useFavorites

// src/hooks/useFavorites.ts
import { useState, useEffect } from 'react';

const FAVORITES_KEY = 'weather-favorites';

export function useFavorites() {
    const [favorites, setFavorites] = useState<string[]>(() => {
        try {
            const stored = localStorage.getItem(FAVORITES_KEY);
            return stored ? JSON.parse(stored) : [];
        } catch {
            return [];
        }
    });

    // Save to localStorage whenever favorites change
    useEffect(() => {
        try {
            localStorage.setItem(FAVORITES_KEY, JSON.stringify(favorites));
        } catch (error) {
            console.error('Failed to save favorites:', error);
        }
    }, [favorites]);

    const addFavorite = (city: string) => {
        setFavorites(prev => {
            if (prev.includes(city)) return prev;
            return [...prev, city];
        });
    };

    const removeFavorite = (city: string) => {
        setFavorites(prev => prev.filter(fav => fav !== city));
    };

    const isFavorite = (city: string): boolean => {
        return favorites.includes(city);
    };

    return {
        favorites,
        addFavorite,
        removeFavorite,
        isFavorite
    };
}

Hook 4: useGeolocation

// src/hooks/useGeolocation.ts
import { useState } from 'react';

interface Coordinates {
    latitude: number;
    longitude: number;
}

interface UseGeolocationReturn {
    coordinates: Coordinates | null;
    loading: boolean;
    error: string | null;
    getCurrentPosition: () => void;
}

export function useGeolocation(): UseGeolocationReturn {
    const [coordinates, setCoordinates] = useState<Coordinates | null>(null);
    const [loading, setLoading] = useState(false);
    const [error, setError] = useState<string | null>(null);

    const getCurrentPosition = () => {
        if (!navigator.geolocation) {
            setError('Geolocation is not supported by your browser');
            return;
        }

        setLoading(true);
        setError(null);

        navigator.geolocation.getCurrentPosition(
            (position) => {
                setCoordinates({
                    latitude: position.coords.latitude,
                    longitude: position.coords.longitude
                });
                setLoading(false);
            },
            (error) => {
                setError('Failed to get your location. Please check permissions.');
                setLoading(false);
            }
        );
    };

    return {
        coordinates,
        loading,
        error,
        getCurrentPosition
    };
}

πŸ’‘ Custom Hooks Benefits

  • useDebounce - Delays search queries until user stops typing
  • useWeather - Encapsulates all weather fetching logic
  • useFavorites - Manages favorite cities with localStorage
  • useGeolocation - Gets user's current location

βœ… Checkpoint

At this point, you should have:

  • βœ… useDebounce hook for delaying search input
  • βœ… useWeather hook for fetching weather data
  • βœ… useFavorites hook for managing favorite cities
  • βœ… useGeolocation hook for getting user location

All the hooks are ready! Time to build UI components. 🎨

🎨 Building Components

Now that we have our custom hooks ready, let's build the UI components that will bring our weather dashboard to life. We'll create reusable, type-safe components that handle different parts of the interface.

Component Architecture

Our dashboard will be composed of several components, each with a specific responsibility:

graph TD A[App] --> B[SearchBar] A --> C[WeatherCard] A --> D[ForecastList] A --> E[FavoritesList] D --> F[ForecastCard] E --> G[FavoriteItem] style A fill:#667eea,stroke:#333,stroke-width:2px,color:#fff style B fill:#48bb78,stroke:#333,stroke-width:2px style C fill:#48bb78,stroke:#333,stroke-width:2px style D fill:#48bb78,stroke:#333,stroke-width:2px style E fill:#48bb78,stroke:#333,stroke-width:2px

πŸ’‘ Component Responsibilities

  • SearchBar - City search with debouncing
  • WeatherCard - Display current weather details
  • ForecastCard - Single day forecast item
  • ForecastList - Container for 5-day forecast
  • FavoritesList - Manage favorite cities

πŸ” SearchBar Component

The SearchBar component handles city search with debounced input, providing real-time feedback while the user types. It uses our useDebounce hook to optimize API calls.

Create SearchBar.tsx

// src/components/SearchBar.tsx
import React, { useState, useEffect } from 'react';
import { useDebounce } from '../hooks/useDebounce';

interface SearchBarProps {
    onSearch: (city: string) => void;
    isLoading?: boolean;
}

const SearchBar: React.FC<SearchBarProps> = ({ onSearch, isLoading = false }) => {
    const [inputValue, setInputValue] = useState('');
    const debouncedValue = useDebounce(inputValue, 500);

    // Trigger search when debounced value changes
    useEffect(() => {
        if (debouncedValue.trim()) {
            onSearch(debouncedValue);
        }
    }, [debouncedValue, onSearch]);

    const handleSubmit = (e: React.FormEvent) => {
        e.preventDefault();
        if (inputValue.trim()) {
            onSearch(inputValue);
        }
    };

    return (
        <form onSubmit={handleSubmit} className="search-bar">
            <div className="search-input-wrapper">
                <input
                    type="text"
                    value={inputValue}
                    onChange={(e) => setInputValue(e.target.value)}
                    placeholder="Search for a city..."
                    className="search-input"
                    disabled={isLoading}
                    aria-label="City search"
                />
                {isLoading && (
                    <span className="search-spinner" aria-label="Searching">
                        πŸ”„
                    </span>
                )}
            </div>
            <button 
                type="submit" 
                className="search-button"
                disabled={isLoading || !inputValue.trim()}
            >
                πŸ” Search
            </button>
        </form>
    );
};

export default SearchBar;

βœ… SearchBar Features

  • Debounced Input - Waits 500ms after typing stops before searching
  • Manual Search - Submit button for immediate search
  • Loading State - Shows spinner and disables input during search
  • Validation - Button disabled when input is empty
  • Accessibility - Proper ARIA labels

🌑️ WeatherCard Component

The WeatherCard displays the current weather for a city, showing temperature, conditions, humidity, wind speed, and more. It uses conditional rendering to show different icons based on weather conditions.

Create WeatherCard.tsx

// src/components/WeatherCard.tsx
import React from 'react';
import { WeatherData } from '../types/weather';

interface WeatherCardProps {
    weather: WeatherData;
    onAddToFavorites?: (city: string) => void;
    isFavorite?: boolean;
}

const WeatherCard: React.FC<WeatherCardProps> = ({ 
    weather, 
    onAddToFavorites,
    isFavorite = false 
}) => {
    // Get weather emoji based on condition
    const getWeatherEmoji = (condition: string): string => {
        const lowerCondition = condition.toLowerCase();
        if (lowerCondition.includes('clear')) return 'β˜€οΈ';
        if (lowerCondition.includes('cloud')) return '☁️';
        if (lowerCondition.includes('rain')) return '🌧️';
        if (lowerCondition.includes('snow')) return '❄️';
        if (lowerCondition.includes('thunder')) return 'β›ˆοΈ';
        if (lowerCondition.includes('mist') || lowerCondition.includes('fog')) return '🌫️';
        return '🌀️';
    };

    // Format temperature
    const formatTemp = (temp: number): string => {
        return `${Math.round(temp)}Β°C`;
    };

    // Format time from timestamp
    const formatTime = (timestamp: number): string => {
        return new Date(timestamp * 1000).toLocaleTimeString('en-US', {
            hour: 'numeric',
            minute: '2-digit'
        });
    };

    return (
        <div className="weather-card">
            {/* Header with city name and favorite button */}
            <div className="weather-header">
                <h2>{weather.name}, {weather.sys.country}</h2>
                {onAddToFavorites && (
                    <button
                        onClick={() => onAddToFavorites(weather.name)}
                        className="favorite-button"
                        aria-label={isFavorite ? 'Remove from favorites' : 'Add to favorites'}
                    >
                        {isFavorite ? '⭐' : 'β˜†'}
                    </button>
                )}
            </div>

            {/* Current time */}
            <p className="weather-time">
                πŸ• {new Date().toLocaleDateString('en-US', {
                    weekday: 'long',
                    year: 'numeric',
                    month: 'long',
                    day: 'numeric'
                })}
            </p>

            {/* Main weather display */}
            <div className="weather-main">
                <div className="weather-icon">
                    {getWeatherEmoji(weather.weather[0].main)}
                </div>
                <div className="weather-temp">
                    <span className="temp-value">{formatTemp(weather.main.temp)}</span>
                    <span className="feels-like">
                        Feels like {formatTemp(weather.main.feels_like)}
                    </span>
                </div>
                <div className="weather-description">
                    {weather.weather[0].description}
                </div>
            </div>

            {/* Weather details grid */}
            <div className="weather-details">
                <div className="detail-item">
                    <span className="detail-label">πŸ’§ Humidity</span>
                    <span className="detail-value">{weather.main.humidity}%</span>
                </div>

                <div className="detail-item">
                    <span className="detail-label">🌬️ Wind Speed</span>
                    <span className="detail-value">{weather.wind.speed} m/s</span>
                </div>

                <div className="detail-item">
                    <span className="detail-label">🎯 Pressure</span>
                    <span className="detail-value">{weather.main.pressure} hPa</span>
                </div>

                <div className="detail-item">
                    <span className="detail-label">πŸ‘οΈ Visibility</span>
                    <span className="detail-value">{(weather.visibility / 1000).toFixed(1)} km</span>
                </div>

                <div className="detail-item">
                    <span className="detail-label">πŸŒ… Sunrise</span>
                    <span className="detail-value">{formatTime(weather.sys.sunrise)}</span>
                </div>

                <div className="detail-item">
                    <span className="detail-label">πŸŒ‡ Sunset</span>
                    <span className="detail-value">{formatTime(weather.sys.sunset)}</span>
                </div>
            </div>

            {/* Min/Max temperatures */}
            <div className="weather-minmax">
                <span>πŸ”» Min: {formatTemp(weather.main.temp_min)}</span>
                <span>πŸ”Ί Max: {formatTemp(weather.main.temp_max)}</span>
            </div>
        </div>
    );
};

export default WeatherCard;

πŸ’‘ WeatherCard Features

  • Dynamic Icons - Shows different emoji based on weather condition
  • Comprehensive Data - Temperature, humidity, wind, pressure, visibility
  • Sunrise/Sunset Times - Formatted local times
  • Favorites Integration - Add/remove from favorites
  • Temperature Formatting - Rounded to nearest degree
  • Feels Like - Shows perceived temperature

πŸ“… ForecastCard Component

The ForecastCard displays weather information for a single day in the forecast. It shows the date, high/low temperatures, and weather conditions.

Create ForecastCard.tsx

// src/components/ForecastCard.tsx
import React from 'react';

interface ForecastItem {
    dt: number;
    main: {
        temp: number;
        temp_min: number;
        temp_max: number;
        humidity: number;
    };
    weather: Array<{
        main: string;
        description: string;
    }>;
    wind: {
        speed: number;
    };
}

interface ForecastCardProps {
    forecast: ForecastItem;
}

const ForecastCard: React.FC<ForecastCardProps> = ({ forecast }) => {
    // Get weather emoji
    const getWeatherEmoji = (condition: string): string => {
        const lowerCondition = condition.toLowerCase();
        if (lowerCondition.includes('clear')) return 'β˜€οΈ';
        if (lowerCondition.includes('cloud')) return '☁️';
        if (lowerCondition.includes('rain')) return '🌧️';
        if (lowerCondition.includes('snow')) return '❄️';
        if (lowerCondition.includes('thunder')) return 'β›ˆοΈ';
        return '🌀️';
    };

    // Format date
    const formatDate = (timestamp: number): string => {
        const date = new Date(timestamp * 1000);
        return date.toLocaleDateString('en-US', {
            weekday: 'short',
            month: 'short',
            day: 'numeric'
        });
    };

    // Format temperature
    const formatTemp = (temp: number): string => {
        return `${Math.round(temp)}Β°`;
    };

    return (
        <div className="forecast-card">
            {/* Date */}
            <div className="forecast-date">
                {formatDate(forecast.dt)}
            </div>

            {/* Weather icon */}
            <div className="forecast-icon">
                {getWeatherEmoji(forecast.weather[0].main)}
            </div>

            {/* Temperature range */}
            <div className="forecast-temps">
                <span className="temp-high">{formatTemp(forecast.main.temp_max)}</span>
                <span className="temp-low">{formatTemp(forecast.main.temp_min)}</span>
            </div>

            {/* Weather description */}
            <div className="forecast-description">
                {forecast.weather[0].description}
            </div>

            {/* Additional details */}
            <div className="forecast-details">
                <small>πŸ’§ {forecast.main.humidity}%</small>
                <small>🌬️ {forecast.wind.speed} m/s</small>
            </div>
        </div>
    );
};

export default ForecastCard;

πŸ“‹ ForecastList Component

The ForecastList component organizes and displays multiple ForecastCard components in a scrollable container.

Create ForecastList.tsx

// src/components/ForecastList.tsx
import React from 'react';
import ForecastCard from './ForecastCard';

interface ForecastData {
    list: Array<{
        dt: number;
        main: {
            temp: number;
            temp_min: number;
            temp_max: number;
            humidity: number;
        };
        weather: Array<{
            main: string;
            description: string;
        }>;
        wind: {
            speed: number;
        };
    }>;
}

interface ForecastListProps {
    forecast: ForecastData;
}

const ForecastList: React.FC<ForecastListProps> = ({ forecast }) => {
    // Group forecast by day (take one entry per day at noon)
    const getDailyForecasts = () => {
        const dailyForecasts = [];
        const seenDates = new Set<string>();

        for (const item of forecast.list) {
            const date = new Date(item.dt * 1000);
            const dateString = date.toDateString();

            // Take the noon forecast for each day (or closest to noon)
            if (!seenDates.has(dateString) || date.getHours() === 12) {
                if (!seenDates.has(dateString)) {
                    seenDates.add(dateString);
                    dailyForecasts.push(item);
                }
            }

            // Limit to 5 days
            if (dailyForecasts.length >= 5) break;
        }

        return dailyForecasts;
    };

    const dailyForecasts = getDailyForecasts();

    return (
        <div className="forecast-list">
            <h3 className="forecast-title">πŸ“… 5-Day Forecast</h3>
            <div className="forecast-grid">
                {dailyForecasts.map((item) => (
                    <ForecastCard key={item.dt} forecast={item} />
                ))}
            </div>
        </div>
    );
};

export default ForecastList;

⚠️ Forecast Data Processing

The OpenWeatherMap 5-day forecast API returns data in 3-hour intervals, giving you 40 data points. We filter this to get one forecast per day (preferably at noon) to create a clean 5-day view.

⭐ FavoritesList Component

The FavoritesList component displays the user's saved favorite cities and allows them to quickly load weather for any favorite.

Create FavoritesList.tsx

// src/components/FavoritesList.tsx
import React from 'react';

interface FavoritesListProps {
    favorites: string[];
    onSelectCity: (city: string) => void;
    onRemoveFavorite: (city: string) => void;
    currentCity?: string;
}

const FavoritesList: React.FC<FavoritesListProps> = ({
    favorites,
    onSelectCity,
    onRemoveFavorite,
    currentCity
}) => {
    if (favorites.length === 0) {
        return (
            <div className="favorites-list empty">
                <p>⭐ No favorite cities yet.</p>
                <p className="favorites-hint">
                    Search for a city and click the star to add it to your favorites!
                </p>
            </div>
        );
    }

    return (
        <div className="favorites-list">
            <h3 className="favorites-title">⭐ Favorite Cities</h3>
            <div className="favorites-grid">
                {favorites.map((city) => (
                    <div 
                        key={city} 
                        className={`favorite-item ${currentCity === city ? 'active' : ''}`}
                    >
                        <button
                            onClick={() => onSelectCity(city)}
                            className="favorite-button"
                            aria-label={`View weather for ${city}`}
                        >
                            πŸ“ {city}
                        </button>
                        <button
                            onClick={(e) => {
                                e.stopPropagation();
                                onRemoveFavorite(city);
                            }}
                            className="remove-favorite"
                            aria-label={`Remove ${city} from favorites`}
                        >
                            βœ•
                        </button>
                    </div>
                ))}
            </div>
        </div>
    );
};

export default FavoritesList;

βœ… FavoritesList Features

  • Empty State - Helpful message when no favorites
  • Quick Access - Click any favorite to load its weather
  • Remove Button - Delete favorites with X button
  • Active State - Highlights currently selected city
  • Event Propagation - stopPropagation prevents conflicts

πŸ”„ Loading and Error Components

Create reusable components for loading states and error messages to provide better user feedback.

Create LoadingSpinner.tsx

// src/components/LoadingSpinner.tsx
import React from 'react';

interface LoadingSpinnerProps {
    message?: string;
}

const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({ 
    message = 'Loading...' 
}) => {
    return (
        <div className="loading-spinner" role="status" aria-live="polite">
            <div className="spinner-icon">πŸ”„</div>
            <p className="loading-message">{message}</p>
        </div>
    );
};

export default LoadingSpinner;

Create ErrorMessage.tsx

// src/components/ErrorMessage.tsx
import React from 'react';

interface ErrorMessageProps {
    message: string;
    onRetry?: () => void;
}

const ErrorMessage: React.FC<ErrorMessageProps> = ({ message, onRetry }) => {
    return (
        <div className="error-message" role="alert">
            <div className="error-icon">❌</div>
            <h3>Oops! Something went wrong</h3>
            <p>{message}</p>
            {onRetry && (
                <button onClick={onRetry} className="retry-button">
                    πŸ”„ Try Again
                </button>
            )}
        </div>
    );
};

export default ErrorMessage;

🎨 Styling the Components

Let's add CSS styles for all our components. Create a comprehensive stylesheet that makes our dashboard look professional and responsive.

Create App.css

/* src/App.css */

/* ===== Global Styles ===== */
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 
                 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 
                 'Helvetica Neue', sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    min-height: 100vh;
    padding: 2rem;
}

.app-container {
    max-width: 1200px;
    margin: 0 auto;
}

/* ===== Header ===== */
.app-header {
    text-align: center;
    color: white;
    margin-bottom: 2rem;
}

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

.app-header p {
    font-size: 1.1rem;
    opacity: 0.9;
}

/* ===== Search Bar ===== */
.search-bar {
    display: flex;
    gap: 1rem;
    margin-bottom: 2rem;
    max-width: 600px;
    margin-left: auto;
    margin-right: auto;
}

.search-input-wrapper {
    flex: 1;
    position: relative;
}

.search-input {
    width: 100%;
    padding: 1rem 3rem 1rem 1rem;
    font-size: 1rem;
    border: none;
    border-radius: 8px;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}

.search-input:focus {
    outline: 2px solid #667eea;
    outline-offset: 2px;
}

.search-spinner {
    position: absolute;
    right: 1rem;
    top: 50%;
    transform: translateY(-50%);
    animation: spin 1s linear infinite;
}

@keyframes spin {
    from { transform: translateY(-50%) rotate(0deg); }
    to { transform: translateY(-50%) rotate(360deg); }
}

.search-button {
    padding: 1rem 2rem;
    background: white;
    border: none;
    border-radius: 8px;
    font-size: 1rem;
    font-weight: 600;
    cursor: pointer;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
    transition: all 0.2s;
}

.search-button:hover:not(:disabled) {
    transform: translateY(-2px);
    box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15);
}

.search-button:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

/* ===== Weather Card ===== */
.weather-card {
    background: white;
    border-radius: 16px;
    padding: 2rem;
    box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
    margin-bottom: 2rem;
}

.weather-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 1rem;
}

.weather-header h2 {
    font-size: 1.8rem;
    color: #333;
}

.favorite-button {
    background: none;
    border: none;
    font-size: 2rem;
    cursor: pointer;
    transition: transform 0.2s;
}

.favorite-button:hover {
    transform: scale(1.2);
}

.weather-time {
    color: #666;
    margin-bottom: 2rem;
}

.weather-main {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 2rem;
    align-items: center;
    margin-bottom: 2rem;
    padding: 2rem;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    border-radius: 12px;
    color: white;
}

.weather-icon {
    font-size: 5rem;
}

.weather-temp {
    display: flex;
    flex-direction: column;
}

.temp-value {
    font-size: 4rem;
    font-weight: 300;
}

.feels-like {
    font-size: 1rem;
    opacity: 0.9;
}

.weather-description {
    grid-column: 1 / -1;
    font-size: 1.3rem;
    text-transform: capitalize;
    text-align: center;
}

.weather-details {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    gap: 1rem;
    margin-bottom: 1rem;
}

.detail-item {
    display: flex;
    flex-direction: column;
    padding: 1rem;
    background: #f8f9fa;
    border-radius: 8px;
}

.detail-label {
    font-size: 0.9rem;
    color: #666;
    margin-bottom: 0.5rem;
}

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

.weather-minmax {
    display: flex;
    justify-content: space-around;
    padding: 1rem;
    background: #f8f9fa;
    border-radius: 8px;
    font-weight: 600;
}

/* ===== Forecast List ===== */
.forecast-list {
    background: white;
    border-radius: 16px;
    padding: 2rem;
    box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
    margin-bottom: 2rem;
}

.forecast-title {
    font-size: 1.5rem;
    margin-bottom: 1.5rem;
    color: #333;
}

.forecast-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    gap: 1rem;
}

/* ===== Forecast Card ===== */
.forecast-card {
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
    padding: 1.5rem;
    border-radius: 12px;
    text-align: center;
    transition: transform 0.2s;
}

.forecast-card:hover {
    transform: translateY(-4px);
}

.forecast-date {
    font-weight: 600;
    margin-bottom: 1rem;
    font-size: 0.9rem;
}

.forecast-icon {
    font-size: 3rem;
    margin-bottom: 1rem;
}

.forecast-temps {
    display: flex;
    justify-content: center;
    gap: 1rem;
    margin-bottom: 0.5rem;
    font-size: 1.3rem;
    font-weight: 600;
}

.temp-high {
    color: #fff;
}

.temp-low {
    opacity: 0.8;
}

.forecast-description {
    text-transform: capitalize;
    margin-bottom: 1rem;
    font-size: 0.9rem;
}

.forecast-details {
    display: flex;
    justify-content: space-around;
    font-size: 0.8rem;
    opacity: 0.9;
}

/* ===== Favorites List ===== */
.favorites-list {
    background: white;
    border-radius: 16px;
    padding: 2rem;
    box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
    margin-bottom: 2rem;
}

.favorites-list.empty {
    text-align: center;
    padding: 3rem 2rem;
    color: #666;
}

.favorites-hint {
    font-size: 0.9rem;
    margin-top: 0.5rem;
}

.favorites-title {
    font-size: 1.5rem;
    margin-bottom: 1.5rem;
    color: #333;
}

.favorites-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    gap: 1rem;
}

.favorite-item {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.75rem 1rem;
    background: #f8f9fa;
    border-radius: 8px;
    transition: all 0.2s;
}

.favorite-item.active {
    background: #667eea;
    color: white;
}

.favorite-item .favorite-button {
    flex: 1;
    background: none;
    border: none;
    text-align: left;
    font-size: 1rem;
    cursor: pointer;
    color: inherit;
}

.remove-favorite {
    background: rgba(255, 0, 0, 0.1);
    border: none;
    width: 24px;
    height: 24px;
    border-radius: 50%;
    cursor: pointer;
    font-size: 0.8rem;
    color: #dc3545;
    transition: all 0.2s;
}

.remove-favorite:hover {
    background: #dc3545;
    color: white;
}

/* ===== Loading Spinner ===== */
.loading-spinner {
    text-align: center;
    padding: 4rem 2rem;
    background: white;
    border-radius: 16px;
    box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
}

.spinner-icon {
    font-size: 4rem;
    margin-bottom: 1rem;
    animation: spin 1s linear infinite;
}

.loading-message {
    font-size: 1.2rem;
    color: #666;
}

/* ===== Error Message ===== */
.error-message {
    text-align: center;
    padding: 3rem 2rem;
    background: white;
    border-radius: 16px;
    box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
}

.error-icon {
    font-size: 4rem;
    margin-bottom: 1rem;
}

.error-message h3 {
    color: #dc3545;
    margin-bottom: 1rem;
}

.error-message p {
    color: #666;
    margin-bottom: 1.5rem;
}

.retry-button {
    padding: 0.75rem 2rem;
    background: #667eea;
    color: white;
    border: none;
    border-radius: 8px;
    font-size: 1rem;
    cursor: pointer;
    transition: all 0.2s;
}

.retry-button:hover {
    background: #5568d3;
    transform: translateY(-2px);
}

/* ===== Responsive Design ===== */
@media (max-width: 768px) {
    body {
        padding: 1rem;
    }

    .app-header h1 {
        font-size: 2rem;
    }

    .search-bar {
        flex-direction: column;
    }

    .weather-main {
        grid-template-columns: 1fr;
        text-align: center;
    }

    .weather-icon {
        font-size: 4rem;
    }

    .temp-value {
        font-size: 3rem;
    }

    .weather-details {
        grid-template-columns: repeat(2, 1fr);
    }

    .forecast-grid {
        grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
    }

    .favorites-grid {
        grid-template-columns: 1fr;
    }
}

βœ… Component Checklist

You should now have created:

  • βœ… SearchBar.tsx - City search with debouncing
  • βœ… WeatherCard.tsx - Current weather display
  • βœ… ForecastCard.tsx - Single day forecast
  • βœ… ForecastList.tsx - 5-day forecast container
  • βœ… FavoritesList.tsx - Favorite cities management
  • βœ… LoadingSpinner.tsx - Loading state
  • βœ… ErrorMessage.tsx - Error handling
  • βœ… App.css - Complete styling

πŸ’‘ Design Principles Applied

  • Component Composition - Small, focused components
  • Props Interface - Clear TypeScript interfaces
  • Conditional Rendering - Show/hide based on state
  • Event Handling - User interactions
  • Accessibility - ARIA labels and semantic HTML
  • Responsive Design - Mobile-first approach

πŸ—οΈ Building the Main App Component

Now it's time to bring everything together! We'll create the main App component that orchestrates all our custom hooks and components, managing the application state and coordinating data flow between all parts of our weather dashboard.

App Component Overview

The App component will be the brain of our application. It will:

πŸ’‘ App Component Responsibilities

  • πŸ”„ Manage overall application state
  • 🌐 Coordinate API calls using custom hooks
  • 🎯 Handle user interactions (search, favorites, geolocation)
  • πŸ“Š Pass data to child components via props
  • ⚠️ Manage global loading and error states
  • πŸ’Ύ Sync favorites with localStorage

Creating App.tsx

Let's build the main App component that ties everything together:

// src/App.tsx
import React, { useState, useCallback, useEffect } from 'react';
import SearchBar from './components/SearchBar';
import WeatherCard from './components/WeatherCard';
import ForecastList from './components/ForecastList';
import FavoritesList from './components/FavoritesList';
import LoadingSpinner from './components/LoadingSpinner';
import ErrorMessage from './components/ErrorMessage';
import { useWeather } from './hooks/useWeather';
import { useFavorites } from './hooks/useFavorites';
import { useGeolocation } from './hooks/useGeolocation';
import './App.css';

const App: React.FC = () => {
    // State for selected city
    const [selectedCity, setSelectedCity] = useState<string>('London');

    // Custom hooks
    const { 
        currentWeather, 
        forecast, 
        loading, 
        error, 
        fetchWeather 
    } = useWeather();

    const { 
        favorites, 
        addFavorite, 
        removeFavorite, 
        isFavorite 
    } = useFavorites();

    const { 
        coordinates, 
        loading: geoLoading, 
        error: geoError, 
        getCurrentPosition 
    } = useGeolocation();

    // Fetch weather when selected city changes
    useEffect(() => {
        if (selectedCity) {
            fetchWeather(selectedCity);
        }
    }, [selectedCity, fetchWeather]);

    // Fetch weather for user's location when coordinates are available
    useEffect(() => {
        if (coordinates) {
            fetchWeather(undefined, coordinates);
        }
    }, [coordinates, fetchWeather]);

    // Handle city search
    const handleSearch = useCallback((city: string) => {
        setSelectedCity(city);
    }, []);

    // Handle favorite city selection
    const handleSelectFavorite = useCallback((city: string) => {
        setSelectedCity(city);
    }, []);

    // Handle add/remove favorite
    const handleToggleFavorite = useCallback((city: string) => {
        if (isFavorite(city)) {
            removeFavorite(city);
        } else {
            addFavorite(city);
        }
    }, [isFavorite, addFavorite, removeFavorite]);

    // Handle retry after error
    const handleRetry = useCallback(() => {
        if (selectedCity) {
            fetchWeather(selectedCity);
        }
    }, [selectedCity, fetchWeather]);

    // Handle get current location
    const handleGetLocation = useCallback(() => {
        getCurrentPosition();
    }, [getCurrentPosition]);

    return (
        <div className="app-container">
            {/* Header */}
            <header className="app-header">
                <h1>🌀️ Weather Dashboard</h1>
                <p>Get real-time weather information for cities worldwide</p>
            </header>

            {/* Search Bar */}
            <SearchBar onSearch={handleSearch} isLoading={loading} />

            {/* Geolocation Button */}
            <div style={{ textAlign: 'center', marginBottom: '2rem' }}>
                <button 
                    onClick={handleGetLocation}
                    disabled={geoLoading}
                    className="location-button"
                    style={{
                        padding: '0.75rem 1.5rem',
                        background: 'white',
                        border: 'none',
                        borderRadius: '8px',
                        fontSize: '1rem',
                        cursor: 'pointer',
                        boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
                        transition: 'all 0.2s'
                    }}
                >
                    πŸ“ {geoLoading ? 'Getting location...' : 'Use My Location'}
                </button>
                {geoError && (
                    <p style={{ color: '#dc3545', marginTop: '0.5rem' }}>
                        {geoError}
                    </p>
                )}
            </div>

            {/* Main Content */}
            {loading && !currentWeather && (
                <LoadingSpinner message="Fetching weather data..." />
            )}

            {error && !loading && (
                <ErrorMessage message={error} onRetry={handleRetry} />
            )}

            {currentWeather && !error && (
                <>
                    {/* Current Weather */}
                    <WeatherCard
                        weather={currentWeather}
                        onAddToFavorites={handleToggleFavorite}
                        isFavorite={isFavorite(currentWeather.name)}
                    />

                    {/* 5-Day Forecast */}
                    {forecast && <ForecastList forecast={forecast} />}

                    {/* Favorites List */}
                    <FavoritesList
                        favorites={favorites}
                        onSelectCity={handleSelectFavorite}
                        onRemoveFavorite={removeFavorite}
                        currentCity={currentWeather.name}
                    />
                </>
            )}
        </div>
    );
};

export default App;

βœ… App Component Features

  • State Management - Tracks selected city
  • Hook Integration - Uses all custom hooks
  • Auto-fetch - Loads weather when city changes
  • Conditional Rendering - Shows loading/error/content states
  • Event Handlers - Memoized with useCallback
  • Geolocation Support - Get weather for current location

🎯 Understanding the Data Flow

Here's how data flows through our application:

sequenceDiagram participant User participant App participant Hooks participant API participant Components User->>App: Enter city name App->>Hooks: Call fetchWeather(city) Hooks->>API: HTTP Request API-->>Hooks: Weather data Hooks-->>App: Update state App->>Components: Pass data via props Components-->>User: Display weather

πŸ”§ Setting Up the Entry Point

Now let's set up the main entry point for our React application:

Update src/main.tsx

// src/main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';

// Create root element
const root = ReactDOM.createRoot(
    document.getElementById('root') as HTMLElement
);

// Render app
root.render(
    <React.StrictMode>
        <App />
    </React.StrictMode>
);

Create src/index.css

/* src/index.css */
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    margin: 0;
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 
                 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 
                 'Helvetica Neue', sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

code {
    font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
                 monospace;
}

#root {
    min-height: 100vh;
}

πŸ“ Environment Variables Setup

Create a .env file in your project root to store your API key securely:

# .env
VITE_OPENWEATHER_API_KEY=your_api_key_here

⚠️ Important: API Key Security

  • Never commit your .env file to version control
  • Add .env to your .gitignore file
  • Use environment variables for sensitive data
  • For production, use backend proxy to hide API keys

Update .gitignore

# .gitignore
# dependencies
node_modules
/.pnp
.pnp.js

# testing
/coverage

# production
/build
/dist

# environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

# misc
.DS_Store
*.log

# IDE
.vscode
.idea

πŸš€ Running Your Application

Now that everything is set up, let's run the application:

# Install dependencies (if not already done)
npm install

# Start the development server
npm run dev

# Your app should open at http://localhost:5173

βœ… Testing Your App

Follow these steps to verify everything works correctly:

βœ… Testing Checklist

  1. Initial Load
    • App loads with default city (London)
    • Current weather displays correctly
    • 5-day forecast shows up
  2. Search Functionality
    • Type a city name (e.g., "Tokyo")
    • Weather updates after 500ms delay
    • Loading spinner shows during fetch
  3. Favorites
    • Click star to add city to favorites
    • Favorites persist after page refresh
    • Click favorite to load that city
    • Click X to remove from favorites
  4. Geolocation
    • Click "Use My Location" button
    • Browser asks for location permission
    • Weather loads for current location
  5. Error Handling
    • Search for invalid city (e.g., "asdfgh")
    • Error message displays
    • Click "Try Again" to retry
  6. Responsive Design
    • Resize browser window
    • Layout adapts to different screen sizes
    • Test on mobile device

πŸ› Common Issues and Solutions

Issue #1: "API key not valid"

Solution:

  • Verify your API key is correct in .env file
  • Check that the variable name is VITE_OPENWEATHER_API_KEY
  • Restart the development server after changing .env
  • Wait a few minutes for new API keys to activate

Issue #2: "City not found"

Solution:

  • Check spelling of city name
  • Try adding country code (e.g., "Paris,FR")
  • Use major cities that are definitely in the database

Issue #3: Geolocation not working

Solution:

  • Ensure you're using HTTPS or localhost
  • Check browser location permissions
  • Some browsers block location on insecure connections

Issue #4: Favorites not persisting

Solution:

  • Check browser's localStorage is enabled
  • Check browser console for errors
  • Clear localStorage and try again
  • Test in different browser

🎨 Optional: Adding Polish

Here are some nice-to-have improvements you can add:

1. Auto-refresh Weather Data

// In App.tsx, add this useEffect
useEffect(() => {
    // Refresh weather every 10 minutes
    const interval = setInterval(() => {
        if (selectedCity) {
            fetchWeather(selectedCity);
        }
    }, 10 * 60 * 1000); // 10 minutes

    return () => clearInterval(interval);
}, [selectedCity, fetchWeather]);

2. Temperature Unit Toggle

// Add state for temperature unit
const [unit, setUnit] = useState<'metric' | 'imperial'>('metric');

// Add toggle button
<button onClick={() => setUnit(unit === 'metric' ? 'imperial' : 'metric')}>
    {unit === 'metric' ? 'Β°C' : 'Β°F'}
</button>

// Pass unit to fetchWeather
fetchWeather(selectedCity, undefined, unit);

3. Weather Alerts

// Show alerts for extreme weather
{currentWeather.main.temp > 35 && (
    <div className="alert alert-danger">
        πŸ”₯ Extreme Heat Warning!
    </div>
)}

{currentWeather.main.temp < 0 && (
    <div className="alert alert-info">
        ❄️ Freezing Temperature Alert!
    </div>
)}

4. Loading Skeleton

// Create a skeleton component for better loading UX
const WeatherSkeleton = () => (
    <div className="weather-card skeleton">
        <div className="skeleton-header"></div>
        <div className="skeleton-main"></div>
        <div className="skeleton-details"></div>
    </div>
);

// Use it instead of LoadingSpinner
{loading && <WeatherSkeleton />}

βœ… Section 8 Complete!

You've now built the complete main App component! Your weather dashboard should be fully functional with:

  • βœ… Full application orchestration
  • βœ… All components integrated
  • βœ… Custom hooks working together
  • βœ… Search, favorites, and geolocation
  • βœ… Error handling and loading states
  • βœ… Responsive design

πŸ§ͺ Testing & Polish

Before we deploy, let's make sure everything works perfectly and add some final polish to our application. Testing ensures reliability, and polish makes the user experience delightful.

Manual Testing Guide

Follow this comprehensive testing checklist to verify all functionality:

🎯 Functional Testing

Search Feature
Test Case Expected Result Pass
Search for valid city (e.g., "Paris") Weather data loads correctly ☐
Search for invalid city Error message displays ☐
Type slowly (debouncing) Search waits 500ms before fetching ☐
Submit with Enter key Search triggers immediately ☐
Favorites Feature
Test Case Expected Result Pass
Click star to add favorite City appears in favorites list ☐
Refresh page Favorites persist ☐
Click favorite city Weather loads for that city ☐
Click X to remove City removed from favorites ☐
Geolocation Feature
Test Case Expected Result Pass
Click "Use My Location" Browser asks for permission ☐
Allow location access Weather loads for current location ☐
Deny location access Error message displays ☐

🎨 UI/UX Testing

Visual Testing Checklist

  • ☐ All weather icons display correctly
  • ☐ Temperature values are properly formatted
  • ☐ Colors and gradients render smoothly
  • ☐ Loading spinner animates properly
  • ☐ Hover effects work on interactive elements
  • ☐ No layout shifts or flickering
  • ☐ Text is readable on all backgrounds
  • ☐ Cards have proper spacing and alignment

πŸ“± Responsive Design Testing

Device Testing

Device/Size Checks Pass
Mobile (320-480px) Single column layout, stacked elements ☐
Tablet (768-1024px) 2-3 column grids, proper spacing ☐
Desktop (1024px+) Full grid layout, max width applied ☐

β™Ώ Accessibility Testing

Accessibility Checklist

  • ☐ All interactive elements are keyboard accessible (Tab navigation)
  • ☐ ARIA labels present on icon buttons
  • ☐ Loading states announced to screen readers (aria-live)
  • ☐ Error messages have proper role="alert"
  • ☐ Color contrast meets WCAG AA standards (4.5:1)
  • ☐ Focus indicators visible on all interactive elements
  • ☐ Form inputs have associated labels

⚑ Performance Testing

Performance Checks

  • ☐ Initial page load under 2 seconds
  • ☐ API calls are debounced (no excessive requests)
  • ☐ No memory leaks (check DevTools Memory tab)
  • ☐ Images and assets optimized
  • ☐ Console has no errors or warnings
  • ☐ Cache working properly (repeated city searches faster)

πŸ’‘ Performance Tips

  • Check Network Tab - Verify API calls aren't duplicated
  • Use React DevTools - Identify unnecessary re-renders
  • Test on Slow 3G - See how app performs on slow connections
  • Monitor localStorage - Ensure it's not growing too large

πŸ” Browser Compatibility

Test Across Browsers

  • ☐ Chrome (latest)
  • ☐ Firefox (latest)
  • ☐ Safari (latest)
  • ☐ Edge (latest)
  • ☐ Mobile browsers (iOS Safari, Chrome Mobile)

βœ… Testing Complete!

Once you've completed all these tests and everything passes, your weather dashboard is ready for deployment! πŸŽ‰

πŸš€ Deployment

Now that your weather dashboard is tested and polished, it's time to share it with the world! We'll deploy our application to a production environment where anyone can access it. We'll cover multiple deployment options, from the easiest to more advanced setups.

Deployment Options Overview

πŸ’‘ Popular Hosting Platforms

Platform Difficulty Free Tier Best For
Vercel ⭐ Easy βœ… Yes React apps, fast deployment
Netlify ⭐ Easy βœ… Yes Static sites, continuous deployment
GitHub Pages ⭐⭐ Medium βœ… Yes Public repos, GitHub integration
AWS Amplify ⭐⭐⭐ Advanced βœ… Limited Scalable apps, AWS ecosystem

🎯 Deployment with Vercel (Recommended)

Vercel is the easiest and fastest way to deploy React applications. It's created by the makers of Next.js and offers automatic deployments from Git repositories.

Step 1: Prepare Your Repository

# Initialize git repository (if not already done)
git init

# Add all files
git add .

# Commit changes
git commit -m "Initial commit: Weather Dashboard"

# Create GitHub repository and push
# (Create repo on GitHub first, then:)
git remote add origin https://github.com/yourusername/weather-dashboard.git
git branch -M main
git push -u origin main

Step 2: Configure Environment Variables

Before deploying, make sure your build works with environment variables:

// Update src/services/weatherService.ts to use Vite env vars
const API_KEY = import.meta.env.VITE_OPENWEATHER_API_KEY;

if (!API_KEY) {
    console.error('API key is missing! Please set VITE_OPENWEATHER_API_KEY');
}

Step 3: Deploy to Vercel

βœ… Vercel Deployment Steps

  1. Sign up - Visit vercel.com and sign up with GitHub
  2. Import Project - Click "New Project" and select your repository
  3. Configure Build Settings:
    • Framework Preset: Vite
    • Build Command: npm run build
    • Output Directory: dist
  4. Add Environment Variables:
    • Key: VITE_OPENWEATHER_API_KEY
    • Value: Your API key
  5. Deploy - Click "Deploy" and wait ~1 minute
  6. Done! - Your app is live at your-app.vercel.app
graph LR A[Push to GitHub] --> B[Vercel Detects Change] B --> C[Build Application] C --> D[Run Tests] D --> E[Deploy to CDN] E --> F[Live at URL] style A fill:#667eea,stroke:#333,stroke-width:2px,color:#fff style F fill:#48bb78,stroke:#333,stroke-width:2px,color:#fff

Step 4: Custom Domain (Optional)

# In Vercel dashboard:
# 1. Go to Project Settings β†’ Domains
# 2. Add your custom domain (e.g., weather.yourdomain.com)
# 3. Follow DNS configuration instructions
# 4. Wait for SSL certificate to be issued (automatic)

🌐 Alternative: Netlify Deployment

Netlify is another excellent option with similar ease of use:

Netlify Deployment Steps

  1. Create netlify.toml configuration file:
    # netlify.toml
    [build]
      command = "npm run build"
      publish = "dist"
    
    [[redirects]]
      from = "/*"
      to = "/index.html"
      status = 200
  2. Sign up at netlify.com
  3. Connect GitHub repository
  4. Add environment variables in Site Settings
  5. Deploy automatically on push

πŸ“¦ GitHub Pages Deployment

For free hosting directly from your GitHub repository:

Install gh-pages Package

npm install --save-dev gh-pages

Update package.json

{
  "name": "weather-dashboard",
  "homepage": "https://yourusername.github.io/weather-dashboard",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview",
    "predeploy": "npm run build",
    "deploy": "gh-pages -d dist"
  }
}

Update vite.config.ts

// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  base: '/weather-dashboard/' // Your repo name
})

Deploy to GitHub Pages

# Deploy to GitHub Pages
npm run deploy

# Your app will be live at:
# https://yourusername.github.io/weather-dashboard

⚠️ GitHub Pages Limitations

  • Environment variables must be handled differently (hardcode or use backend)
  • No server-side functionality
  • Only works with public repositories (free tier)

πŸ” Production Environment Variables

For production deployments, handle environment variables securely:

Best Practices for API Keys

  • Never commit .env files - Always in .gitignore
  • Use platform env vars - Set in Vercel/Netlify dashboard
  • Consider a backend proxy - Hide API keys completely:
    // Instead of calling API directly from frontend:
    // ❌ fetch(`https://api.openweathermap.org/...?appid=${API_KEY}`)
    
    // βœ… Call your backend which handles the API key:
    // fetch(`/api/weather?city=${city}`)
  • Rate limiting - Implement on backend to prevent abuse
  • Rotate keys - Change API keys periodically

🎨 Pre-Deployment Checklist

βœ… Before You Deploy

  • ☐ All tests pass locally
  • ☐ Build succeeds without errors (npm run build)
  • ☐ Preview build works correctly (npm run preview)
  • ☐ Environment variables configured
  • ☐ .gitignore includes .env and node_modules
  • ☐ README.md with project description
  • ☐ License file added (MIT recommended)
  • ☐ All console.logs removed or made conditional
  • ☐ Error boundaries implemented
  • ☐ Analytics added (optional)

πŸ“Š Post-Deployment Monitoring

After deployment, monitor your application's health:

Things to Monitor

  • Error Tracking - Use Sentry or similar service
  • Performance - Use Lighthouse or WebPageTest
  • User Analytics - Google Analytics or Plausible
  • API Usage - Monitor OpenWeatherMap dashboard for quota
  • Uptime - Use UptimeRobot or StatusCake

Adding Error Tracking with Sentry

# Install Sentry
npm install @sentry/react
// src/main.tsx
import * as Sentry from "@sentry/react";

Sentry.init({
    dsn: "your-sentry-dsn",
    environment: import.meta.env.MODE,
    tracesSampleRate: 1.0,
});

// Wrap App in Sentry error boundary
root.render(
    <React.StrictMode>
        <Sentry.ErrorBoundary fallback={<ErrorFallback />}>
            <App />
        </Sentry.ErrorBoundary>
    </React.StrictMode>
);

πŸ”„ Continuous Deployment

Set up automatic deployments for a smooth workflow:

graph TD A[Write Code] --> B[Commit to Git] B --> C[Push to GitHub] C --> D[Vercel/Netlify Detects Push] D --> E[Automated Build] E --> F[Run Tests] F -->|Pass| G[Deploy to Production] F -->|Fail| H[Notify Developer] G --> I[Live Update] style A fill:#667eea,stroke:#333,stroke-width:2px,color:#fff style G fill:#48bb78,stroke:#333,stroke-width:2px,color:#fff style H fill:#dc3545,stroke:#333,stroke-width:2px,color:#fff

πŸ’‘ CI/CD Benefits

  • Automatic Deployments - Push code, it goes live automatically
  • Preview Deployments - Each PR gets its own preview URL
  • Rollback Support - Easy to revert to previous versions
  • No Downtime - Zero-downtime deployments

βœ… Deployment Complete!

Congratulations! Your weather dashboard is now live and accessible to users worldwide. 🌍

What's Next?

  • Share your deployment URL in the course forum
  • Add the project to your portfolio
  • Share on social media with #ReactWeatherDashboard
  • Gather user feedback and iterate

🎁 Bonus Features

Ready to take your weather dashboard to the next level? Here are some exciting features you can add to make your project stand out even more! These are optional challenges that will help you practice advanced React and TypeScript concepts.

πŸŒ“ Feature 1: Dark Mode

Add a beautiful dark mode toggle that persists across sessions.

Create useTheme Hook

// src/hooks/useTheme.ts
import { useState, useEffect } from 'react';

type Theme = 'light' | 'dark';

export const useTheme = () => {
    const [theme, setTheme] = useState<Theme>(() => {
        const saved = localStorage.getItem('theme');
        return (saved as Theme) || 'light';
    });

    useEffect(() => {
        localStorage.setItem('theme', theme);
        document.documentElement.setAttribute('data-theme', theme);
    }, [theme]);

    const toggleTheme = () => {
        setTheme(prev => prev === 'light' ? 'dark' : 'light');
    };

    return { theme, toggleTheme };
};

Add Dark Mode Styles to App.css

/* Dark mode variables */
[data-theme="dark"] {
    --bg-gradient-start: #1a1a2e;
    --bg-gradient-end: #16213e;
    --card-bg: #0f3460;
    --text-primary: #eaeaea;
    --text-secondary: #c5c5c5;
}

[data-theme="light"] {
    --bg-gradient-start: #667eea;
    --bg-gradient-end: #764ba2;
    --card-bg: #ffffff;
    --text-primary: #333333;
    --text-secondary: #666666;
}

body {
    background: linear-gradient(135deg, 
                var(--bg-gradient-start) 0%, 
                var(--bg-gradient-end) 100%);
}

.weather-card {
    background: var(--card-bg);
    color: var(--text-primary);
}

Add Theme Toggle Button

// In App.tsx
const { theme, toggleTheme } = useTheme();

// Add to header
<button 
    onClick={toggleTheme}
    className="theme-toggle"
    aria-label="Toggle theme"
>
    {theme === 'light' ? 'πŸŒ™' : 'β˜€οΈ'}
</button>

πŸ“Š Feature 2: Weather Charts

Visualize temperature trends with interactive charts using Recharts.

Install Recharts

npm install recharts

Create TemperatureChart Component

// src/components/TemperatureChart.tsx
import React from 'react';
import {
    LineChart,
    Line,
    XAxis,
    YAxis,
    CartesianGrid,
    Tooltip,
    ResponsiveContainer
} from 'recharts';

interface ChartData {
    time: string;
    temp: number;
}

interface TemperatureChartProps {
    data: ChartData[];
}

const TemperatureChart: React.FC<TemperatureChartProps> = ({ data }) => {
    return (
        <div className="temperature-chart">
            <h3>πŸ“ˆ Temperature Trend</h3>
            <ResponsiveContainer width="100%" height={300}>
                <LineChart data={data}>
                    <CartesianGrid strokeDasharray="3 3" />
                    <XAxis dataKey="time" />
                    <YAxis />
                    <Tooltip />
                    <Line 
                        type="monotone" 
                        dataKey="temp" 
                        stroke="#667eea" 
                        strokeWidth={2}
                    />
                </LineChart>
            </ResponsiveContainer>
        </div>
    );
};

export default TemperatureChart;

🌑️ Feature 3: Unit Toggle (Celsius ↔ Fahrenheit)

Let users switch between temperature units.

// src/hooks/useTemperatureUnit.ts
import { useState } from 'react';

type Unit = 'metric' | 'imperial';

export const useTemperatureUnit = () => {
    const [unit, setUnit] = useState<Unit>('metric');

    const toggleUnit = () => {
        setUnit(prev => prev === 'metric' ? 'imperial' : 'metric');
    };

    const convertTemp = (celsius: number): number => {
        return unit === 'metric' 
            ? celsius 
            : (celsius * 9/5) + 32;
    };

    const getUnitSymbol = (): string => {
        return unit === 'metric' ? 'Β°C' : 'Β°F';
    };

    return { unit, toggleUnit, convertTemp, getUnitSymbol };
};

πŸ—ΊοΈ Feature 4: Map Integration

Show the searched city on an interactive map using Leaflet.

Install Leaflet

npm install leaflet react-leaflet
npm install --save-dev @types/leaflet

Create WeatherMap Component

// src/components/WeatherMap.tsx
import React from 'react';
import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet';
import 'leaflet/dist/leaflet.css';

interface WeatherMapProps {
    lat: number;
    lon: number;
    cityName: string;
}

const WeatherMap: React.FC<WeatherMapProps> = ({ lat, lon, cityName }) => {
    return (
        <div className="weather-map">
            <h3>πŸ—ΊοΈ Location</h3>
            <MapContainer
                center={[lat, lon]}
                zoom={10}
                style={{ height: '300px', width: '100%', borderRadius: '12px' }}
            >
                <TileLayer
                    url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
                    attribution='© OpenStreetMap contributors'
                />
                <Marker position={[lat, lon]}>
                    <Popup>{cityName}</Popup>
                </Marker>
            </MapContainer>
        </div>
    );
};

export default WeatherMap;

πŸ”” Feature 5: Weather Alerts & Notifications

Add browser notifications for severe weather conditions.

// src/hooks/useNotifications.ts
import { useEffect } from 'react';

export const useNotifications = () => {
    useEffect(() => {
        // Request notification permission
        if ('Notification' in window && Notification.permission === 'default') {
            Notification.requestPermission();
        }
    }, []);

    const sendNotification = (title: string, body: string) => {
        if ('Notification' in window && Notification.permission === 'granted') {
            new Notification(title, {
                body,
                icon: '/weather-icon.png',
                badge: '/badge-icon.png'
            });
        }
    };

    const checkWeatherAlerts = (weather: any) => {
        // Extreme heat
        if (weather.main.temp > 35) {
            sendNotification(
                'πŸ”₯ Extreme Heat Warning!',
                `Temperature in ${weather.name} is ${weather.main.temp}Β°C`
            );
        }

        // Freezing temperatures
        if (weather.main.temp < 0) {
            sendNotification(
                '❄️ Freezing Alert!',
                `Temperature in ${weather.name} is below freezing`
            );
        }

        // High wind speeds
        if (weather.wind.speed > 15) {
            sendNotification(
                'πŸ’¨ High Wind Warning!',
                `Wind speed in ${weather.name} is ${weather.wind.speed} m/s`
            );
        }
    };

    return { sendNotification, checkWeatherAlerts };
};

🌐 Feature 6: Multi-Language Support

Add internationalization (i18n) support.

Install i18next

npm install react-i18next i18next

Configure i18n

// src/i18n.ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';

const resources = {
    en: {
        translation: {
            "search": "Search for a city",
            "current_weather": "Current Weather",
            "forecast": "5-Day Forecast",
            "favorites": "Favorite Cities"
        }
    },
    es: {
        translation: {
            "search": "Buscar una ciudad",
            "current_weather": "Clima Actual",
            "forecast": "PronΓ³stico de 5 DΓ­as",
            "favorites": "Ciudades Favoritas"
        }
    },
    fr: {
        translation: {
            "search": "Rechercher une ville",
            "current_weather": "MΓ©tΓ©o Actuelle",
            "forecast": "PrΓ©visions sur 5 Jours",
            "favorites": "Villes Favorites"
        }
    }
};

i18n
    .use(initReactI18next)
    .init({
        resources,
        lng: 'en',
        fallbackLng: 'en',
        interpolation: {
            escapeValue: false
        }
    });

export default i18n;

⚑ Feature 7: Progressive Web App (PWA)

Make your app installable and work offline.

Install Vite PWA Plugin

npm install vite-plugin-pwa -D

Update vite.config.ts

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { VitePWA } from 'vite-plugin-pwa';

export default defineConfig({
    plugins: [
        react(),
        VitePWA({
            registerType: 'autoUpdate',
            includeAssets: ['favicon.ico', 'robots.txt'],
            manifest: {
                name: 'Weather Dashboard',
                short_name: 'Weather',
                description: 'Real-time weather information worldwide',
                theme_color: '#667eea',
                icons: [
                    {
                        src: 'icon-192.png',
                        sizes: '192x192',
                        type: 'image/png'
                    },
                    {
                        src: 'icon-512.png',
                        sizes: '512x512',
                        type: 'image/png'
                    }
                ]
            }
        })
    ]
});

🎨 Feature 8: Animated Weather Icons

Replace emoji with beautiful animated weather icons.

# Use animated weather icons library
npm install react-animated-weather
// src/components/AnimatedWeatherIcon.tsx
import React from 'react';
import ReactAnimatedWeather from 'react-animated-weather';

interface AnimatedWeatherIconProps {
    condition: string;
}

const AnimatedWeatherIcon: React.FC<AnimatedWeatherIconProps> = ({ condition }) => {
    const getIcon = (condition: string) => {
        const lower = condition.toLowerCase();
        if (lower.includes('clear')) return 'CLEAR_DAY';
        if (lower.includes('cloud')) return 'CLOUDY';
        if (lower.includes('rain')) return 'RAIN';
        if (lower.includes('snow')) return 'SNOW';
        if (lower.includes('thunder')) return 'SLEET';
        return 'PARTLY_CLOUDY_DAY';
    };

    return (
        <ReactAnimatedWeather
            icon={getIcon(condition)}
            color="#667eea"
            size={80}
            animate={true}
        />
    );
};

export default AnimatedWeatherIcon;

βœ… Bonus Features Summary

You now have ideas for 8 powerful features to enhance your weather dashboard:

  1. πŸŒ“ Dark Mode with theme persistence
  2. πŸ“Š Interactive temperature charts
  3. 🌑️ Unit conversion (Β°C ↔ Β°F)
  4. πŸ—ΊοΈ Interactive map with Leaflet
  5. πŸ”” Weather alerts and notifications
  6. 🌐 Multi-language support (i18n)
  7. ⚑ Progressive Web App capabilities
  8. 🎨 Animated weather icons

Challenge: Pick 2-3 features and implement them to make your project truly unique! πŸ’ͺ

πŸ† Project Showcase Ideas

πŸ’‘ How to Present Your Project

  • Portfolio Website - Add to your developer portfolio
  • GitHub README - Write comprehensive documentation
  • Demo Video - Record a walkthrough (Loom or YouTube)
  • Blog Post - Write about your development journey
  • Social Media - Share screenshots and features
  • Dev.to Article - Share lessons learned

πŸ“š Further Learning Resources

Continue Your Journey

πŸŽ‰ Congratulations!

You've completed the Module 4 Weather Dashboard project! This was a comprehensive application that brought together everything you learned in this module and beyond. Let's recap what you've accomplished:

πŸ† What You Built

  • βœ… Complete React + TypeScript Application - Full-stack frontend app
  • βœ… API Integration - Real-time data from OpenWeatherMap
  • βœ… Custom Hooks Library - Reusable logic (useWeather, useFavorites, useDebounce, useGeolocation)
  • βœ… Component Architecture - 7+ well-structured components
  • βœ… State Management - Complex state with multiple hooks
  • βœ… Error Handling - Graceful error states and retry logic
  • βœ… Loading States - Proper UX feedback
  • βœ… Local Storage - Data persistence
  • βœ… Geolocation - Browser API integration
  • βœ… Responsive Design - Mobile-first approach
  • βœ… Accessibility - ARIA labels and semantic HTML
  • βœ… Production Deployment - Live on the web!

πŸ’ͺ Skills You Practiced

  • useEffect - Side effects, cleanup, dependencies
  • Data Fetching - Async/await, error handling
  • Custom Hooks - Extracting and reusing logic
  • API Integration - Working with REST APIs
  • TypeScript - Type safety throughout
  • Component Composition - Building complex UIs
  • State Management - Multiple state sources
  • Performance Optimization - Debouncing, memoization
  • Deployment - Taking apps to production

🎯 Next Steps

  1. Implement Bonus Features - Add dark mode, charts, or maps
  2. Share Your Project - Show it off to friends and on social media
  3. Get Feedback - Ask other developers to review your code
  4. Refactor & Optimize - Clean up code, improve performance
  5. Move to Module 5 - Continue learning advanced concepts
  6. Build More Projects - Apply what you learned to new ideas

🌟 Amazing Work! 🌟

You've built a production-ready weather application from scratch. This is a major milestone in your React and TypeScript journey!

Keep building, keep learning, and keep pushing yourself to create amazing things!

πŸš€ Ready for Module 5? Let's go! πŸš€