Build a complete weather application combining APIs, geolocation, storage, and modern JavaScript. Apply everything you have learned in a real-world project.
In this capstone project, you will build a complete weather application that demonstrates all the concepts learned in this course. The app will fetch weather data from APIs, use geolocation, store user preferences, and provide a polished user experience with proper error handling.
Let's build a comprehensive weather application with modern JavaScript patterns and best practices.
Project Structure:
1weather-app/
2├── index.html
3├── css/
4│ └── style.css
5├── js/
6│ ├── main.js
7│ ├── weather-api.js
8│ ├── geolocation.js
9│ ├── storage.js
10│ └── ui.js
11└── assets/
12 └── icons/HTML Foundation (index.html):
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 <title>Weather App</title>
7 <link rel="stylesheet" href="css/style.css">
8</head>
9<body>
10 <div class="app-container">
11 <header class="app-header">
12 <h1>Weather App</h1>
13 <button id="location-btn" class="btn-primary">
14 📍 Use My Location
15 </button>
16 </header>
17
18 <main class="main-content">
19 <div class="search-section">
20 <div class="search-container">
21 <input type="text" id="city-input" placeholder="Enter city name...">
22 <button id="search-btn" class="btn-primary">Search</button>
23 </div>
24 </div>
25
26 <div class="weather-display" id="weather-display">
27 <div class="loading" id="loading">
28 <div class="spinner"></div>
29 <p>Loading weather data...</p>
30 </div>
31
32 <div class="weather-card" id="weather-card" style="display: none;">
33 <div class="current-weather">
34 <div class="location-info">
35 <h2 id="location-name">--</h2>
36 <p id="current-date">--</p>
37 </div>
38 <div class="temperature-info">
39 <span id="temperature">--</span>
40 <span id="weather-icon">🌤️</span>
41 </div>
42 <div class="weather-details">
43 <p id="weather-description">--</p>
44 <div class="details-grid">
45 <div class="detail-item">
46 <span>Feels like</span>
47 <span id="feels-like">--</span>
48 </div>
49 <div class="detail-item">
50 <span>Humidity</span>
51 <span id="humidity">--</span>
52 </div>
53 <div class="detail-item">
54 <span>Wind</span>
55 <span id="wind-speed">--</span>
56 </div>
57 <div class="detail-item">
58 <span>Pressure</span>
59 <span id="pressure">--</span>
60 </div>
61 </div>
62 </div>
63 </div>
64
65 <div class="forecast-section">
66 <h3>5-Day Forecast</h3>
67 <div class="forecast-container" id="forecast-container">
68 <!-- Forecast items will be populated by JavaScript -->
69 </div>
70 </div>
71 </div>
72
73 <div class="error-message" id="error-message" style="display: none;">
74 <h3>Something went wrong</h3>
75 <p id="error-text">--</p>
76 <button id="retry-btn" class="btn-secondary">Try Again</button>
77 </div>
78 </div>
79
80 <div class="recent-searches">
81 <h3>Recent Searches</h3>
82 <div class="recent-list" id="recent-list">
83 <!-- Recent searches will be populated by JavaScript -->
84 </div>
85 </div>
86 </main>
87
88 <footer class="app-footer">
89 <p>Weather data provided by OpenWeatherMap</p>
90 </footer>
91 </div>
92
93 <script src="js/storage.js"></script>
94 <script src="js/geolocation.js"></script>
95 <script src="js/weather-api.js"></script>
96 <script src="js/ui.js"></script>
97 <script src="js/main.js"></script>
98</body>
99</html>Application Architecture:
1// main.js - Application entry point and coordination
2class WeatherApp {
3 constructor() {
4 this.weatherAPI = new WeatherAPI();
5 this.geoLocation = new GeoLocationManager();
6 this.storage = new StorageManager();
7 this.ui = new UIManager();
8
9 this.initializeApp();
10 }
11
12 async initializeApp() {
13 this.bindEvents();
14 this.loadRecentSearches();
15
16 // Try to load last searched location
17 const lastLocation = this.storage.get('lastLocation');
18 if (lastLocation) {
19 await this.loadWeatherData(lastLocation);
20 }
21 }
22
23 bindEvents() {
24 document.getElementById('search-btn').addEventListener('click', () => {
25 this.handleSearch();
26 });
27
28 document.getElementById('city-input').addEventListener('keypress', (e) => {
29 if (e.key === 'Enter') {
30 this.handleSearch();
31 }
32 });
33
34 document.getElementById('location-btn').addEventListener('click', () => {
35 this.handleLocationRequest();
36 });
37
38 document.getElementById('retry-btn').addEventListener('click', () => {
39 this.handleRetry();
40 });
41 }
42
43 async handleSearch() {
44 const cityInput = document.getElementById('city-input');
45 const cityName = cityInput.value.trim();
46
47 if (!cityName) {
48 this.ui.showError('Please enter a city name');
49 return;
50 }
51
52 await this.loadWeatherData({ type: 'city', name: cityName });
53 cityInput.value = '';
54 }
55
56 async handleLocationRequest() {
57 this.ui.showLoading();
58
59 try {
60 const position = await this.geoLocation.getCurrentPosition();
61 await this.loadWeatherData({
62 type: 'coordinates',
63 lat: position.latitude,
64 lon: position.longitude
65 });
66 } catch (error) {
67 this.ui.showError('Could not get your location: ' + error.message);
68 }
69 }
70
71 async loadWeatherData(location) {
72 this.ui.showLoading();
73
74 try {
75 const weatherData = await this.weatherAPI.getWeatherData(location);
76 const forecastData = await this.weatherAPI.getForecastData(location);
77
78 this.ui.displayWeather(weatherData, forecastData);
79 this.storage.saveRecentSearch(location);
80 this.storage.set('lastLocation', location);
81 this.loadRecentSearches();
82
83 } catch (error) {
84 this.ui.showError(error.message);
85 }
86 }
87
88 loadRecentSearches() {
89 const recentSearches = this.storage.getRecentSearches();
90 this.ui.displayRecentSearches(recentSearches, (location) => {
91 this.loadWeatherData(location);
92 });
93 }
94
95 handleRetry() {
96 const lastLocation = this.storage.get('lastLocation');
97 if (lastLocation) {
98 this.loadWeatherData(lastLocation);
99 }
100 }
101}
102
103// Initialize the app when DOM is loaded
104document.addEventListener('DOMContentLoaded', () => {
105 new WeatherApp();
106});Implement robust API integration with proper error handling and data transformation.
Weather API Manager (weather-api.js):
1class WeatherAPI {
2 constructor() {
3 this.apiKey = 'YOUR_API_KEY_HERE'; // Replace with your OpenWeatherMap API key
4 this.baseURL = 'https://api.openweathermap.org/data/2.5';
5 this.timeout = 10000; // 10 seconds
6 }
7
8 // Get current weather data
9 async getWeatherData(location) {
10 const url = this.buildWeatherURL(location);
11
12 try {
13 const response = await this.fetchWithTimeout(url);
14 return this.processWeatherData(response);
15 } catch (error) {
16 throw this.handleAPIError(error);
17 }
18 }
19
20 // Get 5-day forecast data
21 async getForecastData(location) {
22 const url = this.buildForecastURL(location);
23
24 try {
25 const response = await this.fetchWithTimeout(url);
26 return this.processForecastData(response);
27 } catch (error) {
28 throw this.handleAPIError(error);
29 }
30 }
31
32 // Build weather API URL
33 buildWeatherURL(location) {
34 const params = new URLSearchParams({
35 appid: this.apiKey,
36 units: 'metric'
37 });
38
39 if (location.type === 'city') {
40 params.append('q', location.name);
41 } else if (location.type === 'coordinates') {
42 params.append('lat', location.lat);
43 params.append('lon', location.lon);
44 }
45
46 return this.baseURL + '/weather?' + params.toString();
47 }
48
49 // Build forecast API URL
50 buildForecastURL(location) {
51 const params = new URLSearchParams({
52 appid: this.apiKey,
53 units: 'metric'
54 });
55
56 if (location.type === 'city') {
57 params.append('q', location.name);
58 } else if (location.type === 'coordinates') {
59 params.append('lat', location.lat);
60 params.append('lon', location.lon);
61 }
62
63 return this.baseURL + '/forecast?' + params.toString();
64 }
65
66 // Fetch with timeout
67 async fetchWithTimeout(url) {
68 const controller = new AbortController();
69 const timeoutId = setTimeout(() => controller.abort(), this.timeout);
70
71 try {
72 const response = await fetch(url, {
73 signal: controller.signal
74 });
75
76 clearTimeout(timeoutId);
77
78 if (!response.ok) {
79 const errorData = await response.json();
80 throw new Error(errorData.message || 'API request failed');
81 }
82
83 return await response.json();
84 } catch (error) {
85 clearTimeout(timeoutId);
86
87 if (error.name === 'AbortError') {
88 throw new Error('Request timed out. Please try again.');
89 }
90
91 throw error;
92 }
93 }
94
95 // Process weather data
96 processWeatherData(data) {
97 return {
98 location: {
99 name: data.name,
100 country: data.sys.country,
101 coordinates: {
102 lat: data.coord.lat,
103 lon: data.coord.lon
104 }
105 },
106 current: {
107 temperature: Math.round(data.main.temp),
108 feelsLike: Math.round(data.main.feels_like),
109 humidity: data.main.humidity,
110 pressure: data.main.pressure,
111 windSpeed: data.wind.speed,
112 windDirection: data.wind.deg,
113 description: data.weather[0].description,
114 icon: data.weather[0].icon,
115 timestamp: new Date(data.dt * 1000)
116 },
117 sun: {
118 sunrise: new Date(data.sys.sunrise * 1000),
119 sunset: new Date(data.sys.sunset * 1000)
120 }
121 };
122 }
123
124 // Process forecast data
125 processForecastData(data) {
126 const dailyForecasts = {};
127
128 data.list.forEach(item => {
129 const date = new Date(item.dt * 1000);
130 const dateKey = date.toDateString();
131
132 if (!dailyForecasts[dateKey]) {
133 dailyForecasts[dateKey] = {
134 date: date,
135 temperatures: [],
136 descriptions: [],
137 icons: [],
138 humidity: [],
139 windSpeed: []
140 };
141 }
142
143 dailyForecasts[dateKey].temperatures.push(item.main.temp);
144 dailyForecasts[dateKey].descriptions.push(item.weather[0].description);
145 dailyForecasts[dateKey].icons.push(item.weather[0].icon);
146 dailyForecasts[dateKey].humidity.push(item.main.humidity);
147 dailyForecasts[dateKey].windSpeed.push(item.wind.speed);
148 });
149
150 // Process daily averages
151 return Object.values(dailyForecasts).slice(0, 5).map(day => ({
152 date: day.date,
153 temperature: {
154 min: Math.round(Math.min(...day.temperatures)),
155 max: Math.round(Math.max(...day.temperatures)),
156 avg: Math.round(day.temperatures.reduce((a, b) => a + b, 0) / day.temperatures.length)
157 },
158 description: this.getMostCommonValue(day.descriptions),
159 icon: this.getMostCommonValue(day.icons),
160 humidity: Math.round(day.humidity.reduce((a, b) => a + b, 0) / day.humidity.length),
161 windSpeed: Math.round(day.windSpeed.reduce((a, b) => a + b, 0) / day.windSpeed.length)
162 }));
163 }
164
165 // Get most common value from array
166 getMostCommonValue(array) {
167 const frequency = {};
168 let maxCount = 0;
169 let mostCommon = array[0];
170
171 array.forEach(item => {
172 frequency[item] = (frequency[item] || 0) + 1;
173 if (frequency[item] > maxCount) {
174 maxCount = frequency[item];
175 mostCommon = item;
176 }
177 });
178
179 return mostCommon;
180 }
181
182 // Handle API errors
183 handleAPIError(error) {
184 const errorMessages = {
185 401: 'Invalid API key. Please check your configuration.',
186 404: 'Location not found. Please check the city name.',
187 429: 'Too many requests. Please try again later.',
188 500: 'Weather service is temporarily unavailable.',
189 502: 'Weather service is temporarily unavailable.',
190 503: 'Weather service is temporarily unavailable.'
191 };
192
193 if (error.message.includes('Failed to fetch')) {
194 return new Error('Network error. Please check your internet connection.');
195 }
196
197 if (error.message.includes('timeout')) {
198 return new Error('Request timed out. Please try again.');
199 }
200
201 // Try to extract HTTP status code
202 const statusMatch = error.message.match(/(d{3})/);
203 if (statusMatch) {
204 const statusCode = parseInt(statusMatch[1]);
205 return new Error(errorMessages[statusCode] || error.message);
206 }
207
208 return error;
209 }
210
211 // Get weather icon URL
212 getIconURL(iconCode) {
213 return `https://openweathermap.org/img/wn/${iconCode}@2x.png`;
214 }
215}Implement the remaining components for a fully functional weather application.
Geolocation Manager (geolocation.js):
1class GeoLocationManager {
2 constructor() {
3 this.options = {
4 enableHighAccuracy: true,
5 timeout: 10000,
6 maximumAge: 300000 // 5 minutes
7 };
8 }
9
10 async getCurrentPosition() {
11 if (!this.isSupported()) {
12 throw new Error('Geolocation is not supported by this browser');
13 }
14
15 return new Promise((resolve, reject) => {
16 navigator.geolocation.getCurrentPosition(
17 (position) => {
18 resolve({
19 latitude: position.coords.latitude,
20 longitude: position.coords.longitude,
21 accuracy: position.coords.accuracy
22 });
23 },
24 (error) => {
25 const errorMessages = {
26 1: 'Location access denied by user',
27 2: 'Location unavailable',
28 3: 'Location request timed out'
29 };
30 reject(new Error(errorMessages[error.code] || 'Unknown location error'));
31 },
32 this.options
33 );
34 });
35 }
36
37 isSupported() {
38 return 'geolocation' in navigator;
39 }
40}Storage Manager (storage.js):
1class StorageManager {
2 constructor() {
3 this.storage = localStorage;
4 this.prefix = 'weather_app_';
5 }
6
7 // Set item with optional expiration
8 set(key, value, expirationDays = null) {
9 const data = {
10 value: value,
11 timestamp: Date.now(),
12 expiration: expirationDays ? Date.now() + (expirationDays * 24 * 60 * 60 * 1000) : null
13 };
14
15 try {
16 this.storage.setItem(this.prefix + key, JSON.stringify(data));
17 return true;
18 } catch (error) {
19 console.error('Storage error:', error);
20 return false;
21 }
22 }
23
24 // Get item with expiration check
25 get(key) {
26 try {
27 const item = this.storage.getItem(this.prefix + key);
28 if (!item) return null;
29
30 const data = JSON.parse(item);
31
32 // Check expiration
33 if (data.expiration && Date.now() > data.expiration) {
34 this.remove(key);
35 return null;
36 }
37
38 return data.value;
39 } catch (error) {
40 console.error('Storage retrieval error:', error);
41 return null;
42 }
43 }
44
45 // Remove item
46 remove(key) {
47 this.storage.removeItem(this.prefix + key);
48 }
49
50 // Save recent search
51 saveRecentSearch(location) {
52 const recentSearches = this.getRecentSearches();
53
54 // Remove if already exists
55 const filtered = recentSearches.filter(search =>
56 !(search.type === location.type &&
57 (search.name === location.name ||
58 (search.lat === location.lat && search.lon === location.lon)))
59 );
60
61 // Add to beginning
62 filtered.unshift({
63 ...location,
64 timestamp: Date.now()
65 });
66
67 // Keep only last 5 searches
68 const limited = filtered.slice(0, 5);
69
70 this.set('recentSearches', limited);
71 }
72
73 // Get recent searches
74 getRecentSearches() {
75 return this.get('recentSearches') || [];
76 }
77}UI Manager (ui.js):
1class UIManager {
2 constructor() {
3 this.elements = {
4 loading: document.getElementById('loading'),
5 weatherCard: document.getElementById('weather-card'),
6 errorMessage: document.getElementById('error-message'),
7 locationName: document.getElementById('location-name'),
8 currentDate: document.getElementById('current-date'),
9 temperature: document.getElementById('temperature'),
10 weatherIcon: document.getElementById('weather-icon'),
11 weatherDescription: document.getElementById('weather-description'),
12 feelsLike: document.getElementById('feels-like'),
13 humidity: document.getElementById('humidity'),
14 windSpeed: document.getElementById('wind-speed'),
15 pressure: document.getElementById('pressure'),
16 forecastContainer: document.getElementById('forecast-container'),
17 recentList: document.getElementById('recent-list'),
18 errorText: document.getElementById('error-text')
19 };
20 }
21
22 showLoading() {
23 this.hideAllStates();
24 this.elements.loading.style.display = 'block';
25 }
26
27 showError(message) {
28 this.hideAllStates();
29 this.elements.errorText.textContent = message;
30 this.elements.errorMessage.style.display = 'block';
31 }
32
33 displayWeather(weatherData, forecastData) {
34 this.hideAllStates();
35 this.populateCurrentWeather(weatherData);
36 this.populateForecast(forecastData);
37 this.elements.weatherCard.style.display = 'block';
38 }
39
40 hideAllStates() {
41 this.elements.loading.style.display = 'none';
42 this.elements.weatherCard.style.display = 'none';
43 this.elements.errorMessage.style.display = 'none';
44 }
45
46 populateCurrentWeather(data) {
47 this.elements.locationName.textContent = `${data.location.name}, ${data.location.country}`;
48 this.elements.currentDate.textContent = this.formatDate(new Date());
49 this.elements.temperature.textContent = `${data.current.temperature}°C`;
50 this.elements.weatherDescription.textContent = this.capitalizeWords(data.current.description);
51 this.elements.feelsLike.textContent = `${data.current.feelsLike}°C`;
52 this.elements.humidity.textContent = `${data.current.humidity}%`;
53 this.elements.windSpeed.textContent = `${data.current.windSpeed} m/s`;
54 this.elements.pressure.textContent = `${data.current.pressure} hPa`;
55
56 // Set weather icon
57 this.elements.weatherIcon.textContent = this.getWeatherEmoji(data.current.icon);
58 }
59
60 populateForecast(forecastData) {
61 this.elements.forecastContainer.innerHTML = '';
62
63 forecastData.forEach(day => {
64 const forecastItem = document.createElement('div');
65 forecastItem.className = 'forecast-item';
66 forecastItem.innerHTML = `
67 <div class="forecast-date">${this.formatForecastDate(day.date)}</div>
68 <div class="forecast-icon">${this.getWeatherEmoji(day.icon)}</div>
69 <div class="forecast-temps">
70 <span class="temp-max">${day.temperature.max}°</span>
71 <span class="temp-min">${day.temperature.min}°</span>
72 </div>
73 <div class="forecast-desc">${this.capitalizeWords(day.description)}</div>
74 `;
75 this.elements.forecastContainer.appendChild(forecastItem);
76 });
77 }
78
79 displayRecentSearches(searches, onSearchClick) {
80 this.elements.recentList.innerHTML = '';
81
82 if (searches.length === 0) {
83 this.elements.recentList.innerHTML = '<p class="no-recent">No recent searches</p>';
84 return;
85 }
86
87 searches.forEach(search => {
88 const searchItem = document.createElement('div');
89 searchItem.className = 'recent-item';
90 searchItem.innerHTML = `
91 <span class="recent-name">${this.getSearchDisplayName(search)}</span>
92 <span class="recent-time">${this.formatRelativeTime(search.timestamp)}</span>
93 `;
94
95 searchItem.addEventListener('click', () => onSearchClick(search));
96 this.elements.recentList.appendChild(searchItem);
97 });
98 }
99
100 getSearchDisplayName(search) {
101 if (search.type === 'city') {
102 return search.name;
103 } else {
104 return `Lat: ${search.lat.toFixed(2)}, Lon: ${search.lon.toFixed(2)}`;
105 }
106 }
107
108 formatDate(date) {
109 return date.toLocaleDateString('en-US', {
110 weekday: 'long',
111 year: 'numeric',
112 month: 'long',
113 day: 'numeric'
114 });
115 }
116
117 formatForecastDate(date) {
118 const today = new Date();
119 const tomorrow = new Date(today);
120 tomorrow.setDate(tomorrow.getDate() + 1);
121
122 if (date.toDateString() === today.toDateString()) {
123 return 'Today';
124 } else if (date.toDateString() === tomorrow.toDateString()) {
125 return 'Tomorrow';
126 } else {
127 return date.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' });
128 }
129 }
130
131 formatRelativeTime(timestamp) {
132 const diff = Date.now() - timestamp;
133 const hours = Math.floor(diff / (1000 * 60 * 60));
134 const days = Math.floor(diff / (1000 * 60 * 60 * 24));
135
136 if (days > 0) {
137 return `${days} day${days > 1 ? 's' : ''} ago`;
138 } else if (hours > 0) {
139 return `${hours} hour${hours > 1 ? 's' : ''} ago`;
140 } else {
141 return 'Just now';
142 }
143 }
144
145 capitalizeWords(str) {
146 return str.replace(/wS*/g, (txt) =>
147 txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase()
148 );
149 }
150
151 getWeatherEmoji(iconCode) {
152 const iconMap = {
153 '01d': '☀️', '01n': '🌙',
154 '02d': '⛅', '02n': '☁️',
155 '03d': '☁️', '03n': '☁️',
156 '04d': '☁️', '04n': '☁️',
157 '09d': '🌧️', '09n': '🌧️',
158 '10d': '🌦️', '10n': '🌧️',
159 '11d': '⛈️', '11n': '⛈️',
160 '13d': '❄️', '13n': '❄️',
161 '50d': '🌫️', '50n': '🌫️'
162 };
163 return iconMap[iconCode] || '🌤️';
164 }
165}This complete weather application demonstrates:
The application is production-ready with proper error handling, data validation, and user experience considerations.
Your progress is saved in this browser, on this device only.