Module 5.4: Mini Project 4 - Weather Dashboard

(AI Image Gen): > Prompt: A stunning 3D isometric weather station floating in the cloud. The station features holographic panels displaying “Temperature”, “Humidity”, and “Wind Speed”. One panel shows a bright sun icon, another shows rain. The base connects to a “Server” block via glowing data cables. Weather elements like small clouds and lightning bolts float nearby. High-tech, clean, vibrant orange and blue color palette.
🛠️ Image Assets Script
Run the following script in your terminal to generate placeholder image files:
mkdir -p images/b05-4
touch images/b05-4/b05-4-banner.png
touch images/b05-4/b05-4-app-flow.png
touch images/b05-4/b05-4-final-result.png🎯 Learning Objectives
In this project, we’ll solidify our understanding of useEffect, Custom Hooks, and Data Fetching as we build a weather forecasting application.
Key Features:
- Real-time City Search: Implement city search with Debouncing for a smooth user experience.
- API Integration: Connect to the OpenWeatherMap API to fetch live weather data.
- Auto Refresh: Automatically update weather data every 30 seconds.
- Error Handling: Gracefully handle scenarios where a city isn’t found or network issues occur.
1. Getting Your API Key
To fetch weather data, you’ll need to sign up for a free account on OpenWeatherMap.
- Head over to OpenWeatherMap and create your account.
- Navigate to the My API Keys section.
- Copy your API Key (it looks something like
e4b5...).
Note: Newly generated API Keys can take 10-15 minutes to become active.
2. Project Structure
We’ll leverage the Custom Hooks you learned in Module 5.3.
src/
├── hooks/
│ ├── useDebounce.ts (Created in lesson 5.3)
│ └── useWeather.ts (New - Handles API call logic)
├── components/
│ ├── SearchBar.tsx (Search UI)
│ └── WeatherCard.tsx (Data display UI)
└── App.tsx
(AI Image Gen): > Prompt: A technical flowchart showing the data flow of the Weather App.
- User types in “Search Input”.
- “useDebounce” waits 500ms.
- “useWeather” Hook triggers API Request.
- API returns JSON.
- “WeatherCard” displays data.
- Parallel loop: “setInterval” triggers “useWeather” every 30s. Minimalist, schematic style.
3. Building the useWeather Hook
Let’s separate the API calling and auto-refresh logic from the UI.
File: src/hooks/useWeather.ts
import { useState, useEffect } from 'react';
const API_KEY = 'YOUR_API_KEY_HERE'; // Replace with your key
const BASE_URL = 'https://api.openweathermap.org/data/2.5/weather';
// Define the interface for the API response data
interface WeatherData {
name: string;
main: {
temp: number;
humidity: number;
};
weather: {
description: string;
icon: string;
}[];
wind: {
speed: number;
};
}
export default function useWeather(city: string) {
const [data, setData] = useState<WeatherData | null>(null);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const fetchWeather = async () => {
// No need to fetch if there's no city specified
if (!city) return;
setLoading(true);
setError(null);
try {
const response = await fetch(`${BASE_URL}?q=${city}&appid=${API_KEY}&units=metric&lang=vi`);
if (!response.ok) {
// Throw an error if the city is not found or another HTTP error occurs
throw new Error('City not found');
}
const result: WeatherData = await response.json();
setData(result);
} catch (err: any) {
// Set the error message, defaulting to a generic one if none provided
setError(err.message || 'An error occurred');
setData(null); // Clear previous data on error
} finally {
setLoading(false);
}
};
// Effect 1: Fetch weather data whenever the city changes
useEffect(() => {
fetchWeather();
}, [city]); // Dependency array: re-run effect if 'city' changes
// Effect 2: Set up an interval for auto-refreshing data every 30 seconds
useEffect(() => {
// Don't set up the interval if no city is selected
if (!city) return;
const intervalId = setInterval(() => {
console.log('🔄 Auto-refreshing weather data...');
fetchWeather();
}, 30000); // 30 seconds interval
// Cleanup function: clear the interval when the component unmounts or 'city' changes
return () => clearInterval(intervalId);
}, [city]); // Dependency array: re-run effect if 'city' changes
return { data, loading, error };
}4. Building the UI Components
4.1. SearchBar Component
File: src/components/SearchBar.tsx
interface SearchBarProps {
value: string;
onChange: (value: string) => void;
}
export default function SearchBar({ value, onChange }: SearchBarProps) {
return (
<div className="mb-6">
<input
type="text"
placeholder="Enter city name (e.g., Hanoi)..."
className="w-full p-4 rounded-xl border-2 border-blue-200 focus:border-blue-500 focus:outline-none shadow-sm text-lg"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</div>
);
}4.2. WeatherCard Component
File: src/components/WeatherCard.tsx
// Reuse the interface defined in useWeather.ts or import from shared types
interface WeatherData {
name: string;
main: {
temp: number;
humidity: number;
};
weather: {
description: string;
icon: string;
}[];
wind: {
speed: number;
};
}
interface WeatherCardProps {
data: WeatherData | null;
}
export default function WeatherCard({ data }: WeatherCardProps) {
// Render nothing if no data is available
if (!data) return null;
const { name, main, weather, wind } = data;
// Construct the URL for the weather icon
const iconUrl = `https://openweathermap.org/img/wn/${weather[0].icon}@2x.png`;
return (
<div className="bg-white p-8 rounded-3xl shadow-xl text-center animate-fade-in">
<h2 className="text-3xl font-bold text-gray-800 mb-2">{name}</h2>
<p className="text-gray-500 capitalize">{weather[0].description}</p>
<div className="flex justify-center items-center my-6">
<img src={iconUrl} alt="Weather Icon" className="w-24 h-24" />
<span className="text-6xl font-extrabold text-blue-600">{Math.round(main.temp)}°</span>
</div>
<div className="grid grid-cols-2 gap-4 mt-6">
<div className="bg-blue-50 p-4 rounded-xl">
<p className="text-sm text-gray-500">Humidity</p>
<p className="text-xl font-bold text-blue-800">{main.humidity}%</p>
</div>
<div className="bg-blue-50 p-4 rounded-xl">
<p className="text-sm text-gray-500">Wind</p>
<p className="text-xl font-bold text-blue-800">{wind.speed} m/s</p>
</div>
</div>
</div>
);
}5. Integration (App.tsx)
We’ll use useDebounce to prevent excessive API calls while the user is typing.
File: src/App.tsx
import { useState } from 'react';
import useWeather from './hooks/useWeather';
import useDebounce from './hooks/useDebounce'; // Reuse from Module 5.3
import SearchBar from './components/SearchBar';
import WeatherCard from './components/WeatherCard';
export default function App() {
// Initialize with a default city
const [searchTerm, setSearchTerm] = useState < string > 'Ho Chi Minh';
// The debouncedSearch variable will only update after the user stops typing for 500ms
const debouncedSearch = useDebounce(searchTerm, 500);
// The useWeather hook will react to changes in debouncedSearch
const { data, loading, error } = useWeather(debouncedSearch);
return (
<div className="min-h-screen bg-gradient-to-br from-cyan-500 to-blue-600 flex items-center justify-center p-4">
<div className="w-full max-w-md">
<h1 className="text-4xl font-bold text-white text-center mb-8">Weather App 🌤️</h1>
<SearchBar value={searchTerm} onChange={setSearchTerm} />
{/* Display loading state */}
{loading && <div className="text-white text-center text-xl animate-pulse">Loading data...</div>}
{/* Display error state */}
{error && <div className="bg-red-100 text-red-600 p-4 rounded-xl text-center mb-4">⚠️ {error}</div>}
{/* Display fetched weather data */}
{!loading && !error && <WeatherCard data={data} />}
</div>
</div>
);
}
(AI Image Gen): > Prompt: A realistic smartphone mockup displaying the specific Weather App interface we built. The screen shows “Ho Chi Minh City”, “32°C”, a large sun icon, and humidity/wind grids. The background of the phone screen is a soft blue gradient. The phone is floating against a blurred minimalist living room background.
6. Recap
Congratulations on completing Mini Project 4! You’ve now mastered:
- Data Fetching: Using
fetchandasync/awaitto retrieve real-world data. - Custom Hooks: Encapsulating complex API logic (like in
useWeather) to keep your UI clean. - Debounce Strategy: Optimizing search performance with
useDebounce. - Auto Refresh: Implementing background data updates using
setIntervalin conjunction withuseEffect.
7. Advanced Exercises (Optional)
- 5-Day Forecast: Integrate the Forecast API to display a 5-day weather outlook.
- Persistence: Save the last searched city to
localStorageso it persists across page reloads. - Dynamic Backgrounds: Automatically change the app’s background (sunny, rainy, night) based on the data returned from the API.