Develop a comprehensive weather application using real APIs, data visualization, geolocation services, and responsive design. Master API integration, error handling, and data presentation.
Weather applications are excellent projects for learning API integration, data visualization, and responsive design. They require handling real-world data, managing API keys securely, implementing geolocation services, and presenting complex information in user-friendly interfaces.
In this comprehensive project, you'll build a professional weather dashboard that rivals commercial weather apps. You'll learn to work with multiple APIs, handle network errors gracefully, implement caching strategies, and create beautiful data visualizations.
This project showcases essential skills for modern web development: API integration, error handling, responsive design, and data presentation - all critical for building production-ready applications.
Building a weather dashboard requires careful API integration and robust data management strategies.
API Configuration and Key Management:
1class WeatherAPIConfig {
2 constructor() {
3 this.apis = {
4 openweather: {
5 baseUrl: 'https://api.openweathermap.org/data/2.5',
6 key: this.getApiKey('openweather'),
7 endpoints: {
8 current: '/weather',
9 forecast: '/forecast',
10 onecall: '/onecall'
11 }
12 },
13 weatherapi: {
14 baseUrl: 'https://api.weatherapi.com/v1',
15 key: this.getApiKey('weatherapi'),
16 endpoints: {
17 current: '/current.json',
18 forecast: '/forecast.json',
19 history: '/history.json'
20 }
21 }
22 };
23 }
24
25 getApiKey(provider) {
26 // In production, use environment variables
27 return process.env[`${provider.toUpperCase()}_API_KEY`] ||
28 localStorage.getItem(`${provider}_api_key`) ||
29 this.promptForApiKey(provider);
30 }
31
32 promptForApiKey(provider) {
33 const key = prompt(`Please enter your ${provider} API key:`);
34 if (key) {
35 localStorage.setItem(`${provider}_api_key`, key);
36 }
37 return key;
38 }
39}Comprehensive Weather Data Service:
1class WeatherDataService {
2 constructor() {
3 this.config = new WeatherAPIConfig();
4 this.cache = new WeatherCache();
5 this.requestQueue = new RequestQueue();
6 }
7
8 async getCurrentWeather(lat, lon, units = 'metric') {
9 const cacheKey = `current_${lat}_${lon}_${units}`;
10
11 // Check cache first
12 const cached = this.cache.get(cacheKey);
13 if (cached && !this.cache.isExpired(cacheKey, 10 * 60 * 1000)) { // 10 minutes
14 return cached;
15 }
16
17 try {
18 const response = await this.makeRequest('openweather', 'current', {
19 lat,
20 lon,
21 units,
22 appid: this.config.apis.openweather.key
23 });
24
25 const weatherData = this.normalizeCurrentWeather(response);
26 this.cache.set(cacheKey, weatherData);
27
28 return weatherData;
29 } catch (error) {
30 console.error('Failed to fetch current weather:', error);
31
32 // Return cached data if available, even if expired
33 if (cached) {
34 return { ...cached, isStale: true };
35 }
36
37 throw error;
38 }
39 }
40
41 async getWeatherForecast(lat, lon, days = 5) {
42 const cacheKey = `forecast_${lat}_${lon}_${days}`;
43
44 try {
45 const response = await this.makeRequest('openweather', 'forecast', {
46 lat,
47 lon,
48 cnt: days * 8, // 8 forecasts per day (3-hour intervals)
49 appid: this.config.apis.openweather.key,
50 units: 'metric'
51 });
52
53 const forecast = this.normalizeForecastData(response);
54 this.cache.set(cacheKey, forecast, 60 * 60 * 1000); // Cache for 1 hour
55
56 return forecast;
57 } catch (error) {
58 console.error('Failed to fetch forecast:', error);
59 throw error;
60 }
61 }
62
63 normalizeCurrentWeather(apiResponse) {
64 return {
65 location: {
66 name: apiResponse.name,
67 country: apiResponse.sys.country,
68 coordinates: {
69 lat: apiResponse.coord.lat,
70 lon: apiResponse.coord.lon
71 }
72 },
73 current: {
74 temperature: Math.round(apiResponse.main.temp),
75 feelsLike: Math.round(apiResponse.main.feels_like),
76 humidity: apiResponse.main.humidity,
77 pressure: apiResponse.main.pressure,
78 visibility: apiResponse.visibility / 1000, // Convert to km
79 uvIndex: apiResponse.uvi || null,
80 windSpeed: apiResponse.wind.speed,
81 windDirection: apiResponse.wind.deg,
82 cloudiness: apiResponse.clouds.all,
83 description: apiResponse.weather[0].description,
84 icon: apiResponse.weather[0].icon,
85 sunrise: new Date(apiResponse.sys.sunrise * 1000),
86 sunset: new Date(apiResponse.sys.sunset * 1000)
87 },
88 timestamp: new Date()
89 };
90 }
91
92 async makeRequest(provider, endpoint, params) {
93 const api = this.config.apis[provider];
94 const url = new URL(api.baseUrl + api.endpoints[endpoint]);
95
96 Object.entries(params).forEach(([key, value]) => {
97 url.searchParams.append(key, value);
98 });
99
100 // Add to request queue to handle rate limiting
101 return await this.requestQueue.add(() =>
102 fetch(url.toString()).then(response => {
103 if (!response.ok) {
104 throw new Error(`API request failed: ${response.status}`);
105 }
106 return response.json();
107 })
108 );
109 }
110}Smart Caching System:
1class WeatherCache {
2 constructor() {
3 this.cache = new Map();
4 this.maxSize = 50;
5 this.setupCleanup();
6 }
7
8 set(key, data, ttl = 30 * 60 * 1000) { // Default 30 minutes TTL
9 // Remove oldest entries if cache is full
10 if (this.cache.size >= this.maxSize) {
11 const oldestKey = this.cache.keys().next().value;
12 this.cache.delete(oldestKey);
13 }
14
15 this.cache.set(key, {
16 data,
17 timestamp: Date.now(),
18 ttl
19 });
20 }
21
22 get(key) {
23 const entry = this.cache.get(key);
24 if (!entry) return null;
25
26 if (this.isExpired(key)) {
27 this.cache.delete(key);
28 return null;
29 }
30
31 return entry.data;
32 }
33
34 isExpired(key, customTTL = null) {
35 const entry = this.cache.get(key);
36 if (!entry) return true;
37
38 const ttl = customTTL || entry.ttl;
39 return Date.now() - entry.timestamp > ttl;
40 }
41
42 setupCleanup() {
43 // Clean expired entries every 5 minutes
44 setInterval(() => {
45 for (const [key, entry] of this.cache.entries()) {
46 if (this.isExpired(key)) {
47 this.cache.delete(key);
48 }
49 }
50 }, 5 * 60 * 1000);
51 }
52}This robust API integration handles multiple providers, implements smart caching, and provides fallback mechanisms for reliable weather data access.
Implementing accurate location services enhances user experience by providing relevant local weather information.
Comprehensive Geolocation Service:
1class GeolocationService {
2 constructor() {
3 this.currentPosition = null;
4 this.watchId = null;
5 this.locationHistory = [];
6 this.geocodeCache = new Map();
7 }
8
9 async getCurrentLocation() {
10 if (!navigator.geolocation) {
11 throw new Error('Geolocation is not supported by this browser');
12 }
13
14 const options = {
15 enableHighAccuracy: true,
16 timeout: 10000,
17 maximumAge: 5 * 60 * 1000 // 5 minutes
18 };
19
20 try {
21 const position = await new Promise((resolve, reject) => {
22 navigator.geolocation.getCurrentPosition(resolve, reject, options);
23 });
24
25 this.currentPosition = {
26 lat: position.coords.latitude,
27 lon: position.coords.longitude,
28 accuracy: position.coords.accuracy,
29 timestamp: new Date(position.timestamp)
30 };
31
32 return this.currentPosition;
33 } catch (error) {
34 console.error('Geolocation error:', error);
35
36 // Fallback to IP-based location
37 return await this.getLocationByIP();
38 }
39 }
40
41 async getLocationByIP() {
42 try {
43 const response = await fetch('https://ipapi.co/json/');
44 const data = await response.json();
45
46 return {
47 lat: data.latitude,
48 lon: data.longitude,
49 city: data.city,
50 country: data.country_name,
51 source: 'ip',
52 accuracy: 10000 // IP location is less accurate
53 };
54 } catch (error) {
55 console.error('IP location failed:', error);
56
57 // Ultimate fallback to default location
58 return {
59 lat: 40.7128,
60 lon: -74.0060,
61 city: 'New York',
62 country: 'United States',
63 source: 'default'
64 };
65 }
66 }
67
68 startLocationTracking() {
69 if (!navigator.geolocation) return;
70
71 const options = {
72 enableHighAccuracy: false, // Less battery drain for tracking
73 timeout: 30000,
74 maximumAge: 10 * 60 * 1000 // 10 minutes
75 };
76
77 this.watchId = navigator.geolocation.watchPosition(
78 (position) => {
79 const newPosition = {
80 lat: position.coords.latitude,
81 lon: position.coords.longitude,
82 timestamp: new Date(position.timestamp)
83 };
84
85 // Only update if moved significantly (> 1km)
86 if (this.hasMovedSignificantly(newPosition)) {
87 this.currentPosition = newPosition;
88 this.locationHistory.push(newPosition);
89 this.onLocationChange(newPosition);
90 }
91 },
92 (error) => {
93 console.warn('Location tracking error:', error);
94 },
95 options
96 );
97 }
98
99 stopLocationTracking() {
100 if (this.watchId) {
101 navigator.geolocation.clearWatch(this.watchId);
102 this.watchId = null;
103 }
104 }
105
106 hasMovedSignificantly(newPosition) {
107 if (!this.currentPosition) return true;
108
109 const distance = this.calculateDistance(
110 this.currentPosition.lat,
111 this.currentPosition.lon,
112 newPosition.lat,
113 newPosition.lon
114 );
115
116 return distance > 1000; // 1km threshold
117 }
118
119 calculateDistance(lat1, lon1, lat2, lon2) {
120 const R = 6371e3; // Earth's radius in meters
121 const φ1 = lat1 * Math.PI / 180;
122 const φ2 = lat2 * Math.PI / 180;
123 const Δφ = (lat2 - lat1) * Math.PI / 180;
124 const Δλ = (lon2 - lon1) * Math.PI / 180;
125
126 const a = Math.sin(Δφ / 2) * Math.sin(Δφ / 2) +
127 Math.cos(φ1) * Math.cos(φ2) *
128 Math.sin(Δλ / 2) * Math.sin(Δλ / 2);
129 const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
130
131 return R * c;
132 }
133
134 async reverseGeocode(lat, lon) {
135 const cacheKey = `${lat.toFixed(4)},${lon.toFixed(4)}`;
136
137 if (this.geocodeCache.has(cacheKey)) {
138 return this.geocodeCache.get(cacheKey);
139 }
140
141 try {
142 const response = await fetch(
143 `https://api.openweathermap.org/geo/1.0/reverse?lat=${lat}&lon=${lon}&limit=1&appid=${API_KEY}`
144 );
145
146 const data = await response.json();
147
148 if (data.length > 0) {
149 const location = {
150 name: data[0].name,
151 state: data[0].state,
152 country: data[0].country,
153 localNames: data[0].local_names
154 };
155
156 this.geocodeCache.set(cacheKey, location);
157 return location;
158 }
159 } catch (error) {
160 console.error('Reverse geocoding failed:', error);
161 }
162
163 return null;
164 }
165
166 async searchLocations(query) {
167 try {
168 const response = await fetch(
169 `https://api.openweathermap.org/geo/1.0/direct?q=${encodeURIComponent(query)}&limit=5&appid=${API_KEY}`
170 );
171
172 const data = await response.json();
173
174 return data.map(location => ({
175 name: location.name,
176 state: location.state,
177 country: location.country,
178 lat: location.lat,
179 lon: location.lon,
180 fullName: `${location.name}${location.state ? ', ' + location.state : ''}, ${location.country}`
181 }));
182 } catch (error) {
183 console.error('Location search failed:', error);
184 return [];
185 }
186 }
187
188 onLocationChange(position) {
189 // Override this method to handle location changes
190 console.log('Location changed:', position);
191 }
192}Location Search and Autocomplete:
1class LocationSearchWidget {
2 constructor(inputElement, onLocationSelect) {
3 this.input = inputElement;
4 this.onLocationSelect = onLocationSelect;
5 this.geoService = new GeolocationService();
6 this.searchTimeout = null;
7 this.currentResults = [];
8
9 this.setupUI();
10 this.setupEventListeners();
11 }
12
13 setupUI() {
14 this.input.setAttribute('placeholder', 'Search for a city...');
15 this.input.setAttribute('autocomplete', 'off');
16
17 // Create results dropdown
18 this.dropdown = document.createElement('div');
19 this.dropdown.className = 'location-dropdown';
20 this.dropdown.style.display = 'none';
21
22 this.input.parentNode.insertBefore(this.dropdown, this.input.nextSibling);
23 }
24
25 setupEventListeners() {
26 this.input.addEventListener('input', (e) => {
27 const query = e.target.value.trim();
28
29 clearTimeout(this.searchTimeout);
30
31 if (query.length < 2) {
32 this.hideDropdown();
33 return;
34 }
35
36 this.searchTimeout = setTimeout(() => {
37 this.searchLocations(query);
38 }, 300);
39 });
40
41 this.input.addEventListener('blur', () => {
42 // Delay hiding to allow click on dropdown
43 setTimeout(() => {
44 this.hideDropdown();
45 }, 200);
46 });
47
48 this.input.addEventListener('keydown', (e) => {
49 switch (e.key) {
50 case 'ArrowDown':
51 e.preventDefault();
52 this.navigateResults(1);
53 break;
54 case 'ArrowUp':
55 e.preventDefault();
56 this.navigateResults(-1);
57 break;
58 case 'Enter':
59 e.preventDefault();
60 this.selectCurrentResult();
61 break;
62 case 'Escape':
63 this.hideDropdown();
64 break;
65 }
66 });
67 }
68
69 async searchLocations(query) {
70 try {
71 const locations = await this.geoService.searchLocations(query);
72 this.showResults(locations);
73 } catch (error) {
74 console.error('Location search error:', error);
75 this.showError('Search failed. Please try again.');
76 }
77 }
78
79 showResults(locations) {
80 this.currentResults = locations;
81
82 if (locations.length === 0) {
83 this.dropdown.innerHTML = '<div class="no-results">No locations found</div>';
84 } else {
85 this.dropdown.innerHTML = locations
86 .map((location, index) => `
87 <div class="location-result" data-index="${index}">
88 <div class="location-name">${location.name}</div>
89 <div class="location-details">${location.state ? location.state + ', ' : ''}${location.country}</div>
90 </div>
91 `)
92 .join('');
93
94 // Add click listeners
95 this.dropdown.querySelectorAll('.location-result').forEach((result, index) => {
96 result.addEventListener('click', () => {
97 this.selectResult(index);
98 });
99 });
100 }
101
102 this.dropdown.style.display = 'block';
103 }
104
105 selectResult(index) {
106 const location = this.currentResults[index];
107 if (location) {
108 this.input.value = location.fullName;
109 this.hideDropdown();
110 this.onLocationSelect(location);
111 }
112 }
113
114 hideDropdown() {
115 this.dropdown.style.display = 'none';
116 }
117}This comprehensive geolocation system provides accurate location detection, search functionality, and smooth user experience for location-based weather services.
Your progress is saved in this browser, on this device only.