Introduction
For this weekend project, you'll build a complete Progressive Web Application (PWA) using a frontend framework of your choice (React, Vue, or Angular). What makes this project special is that we'll approach it using George Polya's famous 4-step problem-solving procedure, which will help structure your work and ensure you build a robust application.
George Polya (1887-1985) was a renowned mathematician who developed a 4-step approach to problem solving that has been widely adopted in mathematics, computer science, and engineering. His method helps break down complex problems into manageable parts, which is perfect for a project like building a PWA.
Project Overview: Weather Dashboard PWA
For this project, you'll create a Weather Dashboard PWA that allows users to:
- View current weather and forecasts for multiple locations
- Save favorite locations for quick access
- View weather information offline for previously loaded locations
- Receive notifications about severe weather alerts
- Install the application on their devices
You'll implement this application using the following technologies:
- A frontend framework of your choice (React, Vue, or Angular)
- Service Workers for offline functionality
- IndexedDB for local data storage
- Web App Manifest for installability
- The OpenWeatherMap API or similar weather API of your choice
Step 1: Understand the Problem
Following Polya's method, our first step is to thoroughly understand the problem before attempting to solve it. This involves:
Requirements Analysis
Functional Requirements:
- Display current weather conditions (temperature, humidity, wind, etc.)
- Show 5-day forecast for selected locations
- Allow users to search for locations by city name
- Save favorite locations for quick access
- Work offline for previously viewed locations
- Notify users about severe weather alerts
- Be installable as a standalone application
- Automatically detect user's location (with permission)
Non-Functional Requirements:
- Fast loading times (under 2 seconds for initial load)
- Responsive design that works on all device sizes
- Accessible interface that meets WCAG 2.1 AA standards
- Minimal data usage for mobile users
- Battery-efficient background operations
User Personas
Sarah - The Commuter
Age: 32
Occupation: Marketing Manager
Tech Savvy: Medium
Goals: Quickly check weather before commuting, receive alerts about weather affecting her commute, save minimal data on her phone plan
Frustrations: Slow apps, having to search the same locations repeatedly, weather apps that drain battery
Marcus - The Outdoor Enthusiast
Age: 28
Occupation: Freelance Photographer
Tech Savvy: High
Goals: Check detailed weather for various locations, plan outdoor photoshoots based on weather, access weather info in remote areas with poor connectivity
Frustrations: Apps that don't work offline, inaccurate forecasts, complex interfaces
Key Questions to Answer
Before moving forward, make sure you understand:
- What specific weather data will be most valuable to users?
- How much data needs to be cached for offline use?
- What is the optimal UI layout for different device sizes?
- How will users interact with saved locations?
- What permissions will the app need from users?
- How will the app handle API limitations and errors?
Understanding Exercise
Take 30 minutes to create a mind map or list of all aspects of the problem. Include both technical requirements and user needs. Identify any assumptions you're making and questions that remain unanswered.
Step 2: Devise a Plan
With a solid understanding of the problem, we can now devise a comprehensive plan for building our Weather Dashboard PWA. This includes architecture decisions, component breakdown, and implementation strategy.
Technical Architecture
Frontend Framework Selection
Choose the framework you're most comfortable with. Here's a quick comparison to help you decide:
| Framework | Strengths | Considerations | Best For |
|---|---|---|---|
| React | Large ecosystem, flexible, strong community support | Requires additional libraries for routing and state management | Developers who prefer flexibility and component-based architecture |
| Vue | Gentle learning curve, built-in state management, simple syntax | Smaller ecosystem compared to React | Developers who prefer an all-in-one solution with clear conventions |
| Angular | Comprehensive framework, TypeScript integration, dependency injection | Steeper learning curve, more opinionated | Developers who prefer a full-featured framework with strong structure |
Component Breakdown
Regardless of the framework you choose, break down your application into these key components:
Offline Strategy
Plan your offline functionality:
- Service Worker Setup: Register a service worker during the initial load
- Cache Strategy:
- App Shell: Cache-first strategy
- API Data: Network-first with fallback to cache
- Images: Cache-first with network update
- Data Storage:
- IndexedDB for weather data and user preferences
- Structure with separate object stores for locations, weather data, and settings
- Sync Strategy: Use Background Sync API to update cached data when online
Implementation Phases
Break down the project into manageable phases:
- Phase 1: Core Application
- Set up the frontend framework project structure
- Create basic UI components
- Implement weather API integration
- Build location search functionality
- Phase 2: PWA Features
- Configure service worker for offline caching
- Implement IndexedDB for data persistence
- Create Web App Manifest
- Add install promotion
- Phase 3: Enhanced Features
- Add push notifications for weather alerts
- Implement background sync
- Add geolocation for automatic local weather
- Create settings for user preferences
- Phase 4: Polish
- Optimize performance
- Enhance accessibility
- Add animations and transitions
- Implement error handling and fallbacks
Planning Exercise
Create a detailed component breakdown for your chosen framework. Define the props/inputs and outputs for each component. Draft a timeline for implementation phases, allocating time for each feature based on complexity.
Step 3: Execute the Plan
Now it's time to implement our Weather Dashboard PWA according to the plan. We'll examine key implementation aspects for each framework, but remember to apply the concepts to your chosen framework.
Project Setup
Start by setting up your project using the appropriate CLI tool:
React (with Create React App)
# Create the project
npx create-react-app weather-dashboard --template cra-template-pwa
# Navigate to project folder
cd weather-dashboard
# Install additional dependencies
npm install react-router-dom axios styled-components
Vue (with Vue CLI)
# Install Vue CLI if you haven't
npm install -g @vue/cli
# Create the project with PWA plugin
vue create weather-dashboard
# (Select Manual, then choose PWA)
# Navigate to project folder
cd weather-dashboard
# Install additional dependencies
npm install axios vue-router
Angular (with Angular CLI)
# Install Angular CLI if you haven't
npm install -g @angular/cli
# Create the project
ng new weather-dashboard --routing
# Navigate to project folder
cd weather-dashboard
# Add PWA support
ng add @angular/pwa
# Install additional dependencies
npm install @angular/material
API Integration
Set up a service to interact with the weather API. Here's an example using the OpenWeatherMap API:
React API Service
// src/services/weatherService.js
import axios from 'axios';
const API_KEY = 'your_api_key';
const BASE_URL = 'https://api.openweathermap.org/data/2.5';
const weatherService = {
async getCurrentWeather(city) {
try {
const response = await axios.get(`${BASE_URL}/weather`, {
params: {
q: city,
units: 'metric',
appid: API_KEY
}
});
return response.data;
} catch (error) {
console.error('Error fetching current weather:', error);
throw error;
}
},
async getForecast(city) {
try {
const response = await axios.get(`${BASE_URL}/forecast`, {
params: {
q: city,
units: 'metric',
appid: API_KEY
}
});
// Group forecast by day
const forecastByDay = [];
const groupedData = {};
response.data.list.forEach(item => {
const date = new Date(item.dt * 1000).toLocaleDateString();
if (!groupedData[date]) {
groupedData[date] = [];
}
groupedData[date].push(item);
});
// Get average values for each day
Object.keys(groupedData).forEach(date => {
const dayData = groupedData[date];
const temperatures = dayData.map(item => item.main.temp);
const avgTemp = temperatures.reduce((sum, temp) => sum + temp, 0) / temperatures.length;
// Use the weather of noon as representative
const noonData = dayData.find(item => {
const hour = new Date(item.dt * 1000).getHours();
return hour >= 11 && hour <= 13;
}) || dayData[0];
forecastByDay.push({
date,
avgTemp,
weather: noonData.weather[0],
humidity: noonData.main.humidity,
wind: noonData.wind.speed
});
});
return forecastByDay.slice(0, 5); // Return 5-day forecast
} catch (error) {
console.error('Error fetching forecast:', error);
throw error;
}
}
};
export default weatherService;
Implementing Core Components
Create the key components for your application. Here's a React example of the WeatherDisplay component:
// src/components/WeatherDisplay.js
import React, { useState, useEffect } from 'react';
import styled from 'styled-components';
import weatherService from '../services/weatherService';
import CurrentConditions from './CurrentConditions';
import WeatherDetails from './WeatherDetails';
import { saveToIndexedDB } from '../utils/indexedDBHelpers';
const WeatherContainer = styled.div`
background: linear-gradient(to bottom, #57c1eb 0%,#246fa8 100%);
border-radius: 10px;
box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23);
color: white;
padding: 20px;
margin-bottom: 20px;
`;
const LoadingContainer = styled.div`
display: flex;
justify-content: center;
align-items: center;
height: 200px;
width: 100%;
`;
const ErrorContainer = styled.div`
background-color: #ff6b6b;
color: white;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
`;
const WeatherDisplay = ({ location }) => {
const [currentWeather, setCurrentWeather] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
if (!location) return;
const fetchWeatherData = async () => {
try {
setLoading(true);
setError(null);
// Try to get from cached data first if offline
if (!navigator.onLine) {
// Implementation will be covered in the IndexedDB section
return;
}
const data = await weatherService.getCurrentWeather(location);
setCurrentWeather(data);
// Save to IndexedDB for offline access
saveToIndexedDB('weatherData', {
id: location,
data: data,
timestamp: Date.now()
});
} catch (err) {
setError('Failed to load weather data. Please try again later.');
console.error(err);
} finally {
setLoading(false);
}
};
fetchWeatherData();
}, [location]);
if (loading) {
return (
);
}
if (error) {
return (
{error}
);
}
if (!currentWeather) {
return null;
}
return (
);
};
export default WeatherDisplay;
Setting Up IndexedDB
Create a utility file for IndexedDB operations:
// src/utils/indexedDBHelpers.js
const DB_NAME = 'weatherDashboardDB';
const DB_VERSION = 1;
// Initialize the database
export const initDB = () => {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onerror = (event) => {
reject('Error opening IndexedDB');
};
request.onsuccess = (event) => {
const db = event.target.result;
resolve(db);
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
// Create object stores
if (!db.objectStoreNames.contains('weatherData')) {
db.createObjectStore('weatherData', { keyPath: 'id' });
}
if (!db.objectStoreNames.contains('forecastData')) {
db.createObjectStore('forecastData', { keyPath: 'id' });
}
if (!db.objectStoreNames.contains('savedLocations')) {
const savedLocationsStore = db.createObjectStore('savedLocations', { keyPath: 'id' });
savedLocationsStore.createIndex('name', 'name', { unique: false });
}
if (!db.objectStoreNames.contains('settings')) {
db.createObjectStore('settings', { keyPath: 'id' });
}
};
});
};
// Save data to IndexedDB
export const saveToIndexedDB = (storeName, data) => {
return new Promise((resolve, reject) => {
initDB()
.then(db => {
const transaction = db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
const request = store.put(data);
request.onsuccess = () => {
resolve();
};
request.onerror = () => {
reject('Error saving to IndexedDB');
};
})
.catch(error => {
reject(error);
});
});
};
// Get data from IndexedDB
export const getFromIndexedDB = (storeName, id) => {
return new Promise((resolve, reject) => {
initDB()
.then(db => {
const transaction = db.transaction([storeName], 'readonly');
const store = transaction.objectStore(storeName);
const request = store.get(id);
request.onsuccess = () => {
resolve(request.result);
};
request.onerror = () => {
reject('Error reading from IndexedDB');
};
})
.catch(error => {
reject(error);
});
});
};
// Get all data from a store
export const getAllFromIndexedDB = (storeName) => {
return new Promise((resolve, reject) => {
initDB()
.then(db => {
const transaction = db.transaction([storeName], 'readonly');
const store = transaction.objectStore(storeName);
const request = store.getAll();
request.onsuccess = () => {
resolve(request.result);
};
request.onerror = () => {
reject('Error reading from IndexedDB');
};
})
.catch(error => {
reject(error);
});
});
};
// Delete data from IndexedDB
export const deleteFromIndexedDB = (storeName, id) => {
return new Promise((resolve, reject) => {
initDB()
.then(db => {
const transaction = db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
const request = store.delete(id);
request.onsuccess = () => {
resolve();
};
request.onerror = () => {
reject('Error deleting from IndexedDB');
};
})
.catch(error => {
reject(error);
});
});
};
Service Worker Implementation
Customize the service worker to handle offline capabilities for your weather app:
// public/service-worker.js
const CACHE_NAME = 'weather-dashboard-v1';
const STATIC_CACHE_URLS = [
'/',
'/index.html',
'/static/js/main.chunk.js',
'/static/js/0.chunk.js',
'/static/js/bundle.js',
'/manifest.json',
'/favicon.ico',
'/logo192.png',
'/logo512.png'
];
// Install event - cache the app shell
self.addEventListener('install', event => {
console.log('Service Worker installing');
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
console.log('Caching app shell');
return cache.addAll(STATIC_CACHE_URLS);
})
);
});
// Activate event - clean up old caches
self.addEventListener('activate', event => {
console.log('Service Worker activating');
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheName !== CACHE_NAME) {
console.log('Removing old cache:', cacheName);
return caches.delete(cacheName);
}
})
);
})
);
});
// Fetch event - serve from cache, fall back to network
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
// Skip cross-origin requests
if (url.origin !== self.location.origin) {
return;
}
// OpenWeatherMap API requests - network first strategy
if (url.hostname === 'api.openweathermap.org') {
event.respondWith(
fetch(event.request)
.then(response => {
// Clone the response for caching
const responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(cache => {
cache.put(event.request, responseToCache);
});
return response;
})
.catch(() => {
// If network fails, try to get from cache
return caches.match(event.request);
})
);
return;
}
// Weather icons from OpenWeatherMap - cache first strategy
if (url.hostname === 'openweathermap.org' && url.pathname.includes('/img/')) {
event.respondWith(
caches.match(event.request)
.then(cachedResponse => {
if (cachedResponse) {
return cachedResponse;
}
return fetch(event.request)
.then(response => {
// Clone the response for caching
const responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(cache => {
cache.put(event.request, responseToCache);
});
return response;
});
})
);
return;
}
// For HTML navigation requests - cache first with network fallback
if (event.request.mode === 'navigate') {
event.respondWith(
caches.match('/index.html')
.then(cachedResponse => {
return cachedResponse || fetch(event.request);
})
.catch(() => {
return caches.match('/index.html');
})
);
return;
}
// For other static assets - cache first strategy
event.respondWith(
caches.match(event.request)
.then(cachedResponse => {
if (cachedResponse) {
return cachedResponse;
}
return fetch(event.request)
.then(response => {
// Don't cache non-successful responses
if (!response || response.status !== 200) {
return response;
}
// Clone the response for caching
const responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(cache => {
cache.put(event.request, responseToCache);
});
return response;
});
})
);
});
// Background sync for offline operations
self.addEventListener('sync', event => {
if (event.tag === 'sync-saved-locations') {
event.waitUntil(syncSavedLocations());
}
});
// Push notification event handler
self.addEventListener('push', event => {
const data = event.data.json();
const options = {
body: data.body,
icon: '/logo192.png',
badge: '/logo192.png',
data: {
url: data.url || '/'
}
};
event.waitUntil(
self.registration.showNotification(data.title, options)
);
});
// Notification click event handler
self.addEventListener('notificationclick', event => {
event.notification.close();
if (event.notification.data && event.notification.data.url) {
event.waitUntil(
clients.openWindow(event.notification.data.url)
);
}
});
Web App Manifest
Configure your manifest.json file for installability:
// public/manifest.json
{
"short_name": "Weather",
"name": "Weather Dashboard",
"description": "A progressive web application for weather forecasts and alerts",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192",
"purpose": "any maskable"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#246fa8",
"background_color": "#ffffff",
"orientation": "portrait",
"categories": ["weather", "utilities"],
"screenshots": [
{
"src": "screenshot1.png",
"sizes": "1280x720",
"type": "image/png"
},
{
"src": "screenshot2.png",
"sizes": "1280x720",
"type": "image/png"
}
]
}
Implementation Exercise
Choose a specific aspect of the application to implement based on your framework of choice. For example, create the CurrentConditions component or implement the saved locations functionality with IndexedDB storage. Focus on making it work offline by properly caching API responses.
Step 4: Review and Extend
The final step in Polya's approach is to review your solution and look for ways to improve it. This is when you'll test your application, optimize its performance, and add enhancements.
Testing
Test your application thoroughly to ensure it works as expected:
- Offline Testing:
- Use Chrome DevTools to simulate offline conditions
- Test loading saved locations without network
- Verify that the app shell loads instantly from cache
- Progressive Enhancement Testing:
- Test with service workers disabled
- Test with JavaScript disabled (ensure basic content is visible)
- Test with slow network connections
- Installability Testing:
- Verify the install prompt appears
- Install the app and test standalone functionality
- Test app launch from home screen
- Cross-Browser Testing:
- Test in Chrome, Firefox, Safari, and Edge
- Test on iOS and Android devices
Performance Optimization
Measure and improve your application's performance:
- Lighthouse Audit: Run a Lighthouse audit in Chrome DevTools to identify issues
- Bundle Optimization:
- Code splitting for route-based loading
- Tree shaking to eliminate unused code
- Minification and compression
- Image Optimization:
- Use responsive images with srcset
- Compress images and convert to modern formats (WebP)
- Implement lazy loading for images
- Network Optimization:
- Implement request batching
- Use HTTP/2 for multiplexing
- Configure proper cache headers
Accessibility Improvements
Ensure your application is accessible to all users:
- Add proper ARIA attributes where needed
- Ensure sufficient color contrast
- Provide text alternatives for non-text content
- Make all functionality available via keyboard
- Test with screen readers
Feature Extensions
Consider adding these enhancements to your application:
- Weather Radar Map: Integrate with a mapping API to show weather patterns
- Historical Data: Add charts showing historical weather data for locations
- Weather Comparisons: Allow users to compare weather between multiple locations
- Custom Alerts: Let users set up custom alert conditions (e.g., rain tomorrow)
- Weather-based Recommendations: Suggest activities based on the forecast
- Social Sharing: Allow users to share forecasts with friends
- Widget Support: Create a widget version for home screens or embeds
Review Exercise
Run a Lighthouse audit on your application and identify at least three improvements you can make. Implement these improvements and measure the performance impact. Also, choose one extension feature from the list above and implement it.
Project Deliverables
By the end of this weekend project, you should have:
- A functioning Weather Dashboard PWA built with your chosen frontend framework
- Service worker implementation with offline capabilities
- IndexedDB data storage for saved locations and weather data
- A proper Web App Manifest allowing installation
- Clean, documented code following best practices
- A README.md file explaining your application and implementation decisions
Bonus Deliverables
- Automated tests for core functionality
- Push notification implementation for weather alerts
- Background sync for offline data updates
- Deployment to a hosting service (Netlify, Vercel, Firebase, etc.)
Applying Polya's Method Beyond This Project
George Polya's 4-step method is a valuable problem-solving approach that you can apply to any development project:
- Understand the Problem: Always take time to thoroughly analyze requirements, user needs, and constraints before writing any code
- Devise a Plan: Create a clear architecture and implementation strategy before diving into coding
- Execute the Plan: Follow your plan while remaining flexible enough to adjust when obstacles arise
- Review and Extend: Never consider a solution complete without testing, optimization, and reflection on potential improvements
This disciplined approach leads to more robust, maintainable solutions and reduces the need for major refactoring later.
As you continue your development career, cultivate the habit of applying these steps to problems of all sizes, from small bug fixes to large-scale applications.
Resources
APIs
- OpenWeatherMap API - Free weather data API
- WeatherAPI - Another weather data provider with a free tier