Add Progressive Web App functionality
- Add web app manifest for home screen installation - Implement service worker with offline caching strategy - Create offline fallback page with auto-reconnect - Generate PWA icons in multiple sizes (72px-512px) - Add PWA meta tags and Apple Touch icons to all pages - Register service worker with graceful degradation - Update documentation with PWA installation instructions - Add browserconfig.xml for Windows tile support Features: - Installable on mobile and desktop - Offline functionality with cached resources - App-like experience in standalone mode - Automatic updates when online - Works seamlessly with existing progressive enhancement 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
10c6e54062
commit
c13b61cd03
21 changed files with 940 additions and 15 deletions
115
scripts/generate-icons.js
Normal file
115
scripts/generate-icons.js
Normal file
|
@ -0,0 +1,115 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Simple PWA icon generator for Great Lakes Ice Report
|
||||
* Creates basic icons using Canvas API for different sizes
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Icon sizes needed for PWA
|
||||
const SIZES = [72, 96, 128, 144, 152, 192, 384, 512];
|
||||
const MASKABLE_SIZES = [192, 512];
|
||||
|
||||
// Create icons directory if it doesn't exist
|
||||
const iconsDir = path.join(__dirname, '../public/icons');
|
||||
if (!fs.existsSync(iconsDir)) {
|
||||
fs.mkdirSync(iconsDir, { recursive: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate SVG icon content
|
||||
*/
|
||||
function generateSVG(size, isMaskable = false) {
|
||||
const padding = isMaskable ? size * 0.1 : 0; // 10% padding for maskable icons
|
||||
const iconSize = size - (padding * 2);
|
||||
const iconOffset = padding;
|
||||
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#2196F3;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#1976D2;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Background circle -->
|
||||
<circle cx="${size/2}" cy="${size/2}" r="${size/2}" fill="url(#bg)"/>
|
||||
|
||||
<!-- Ice crystal/snowflake icon -->
|
||||
<g transform="translate(${iconOffset + iconSize/2}, ${iconOffset + iconSize/2})">
|
||||
<!-- Main cross -->
|
||||
<line x1="${-iconSize*0.3}" y1="0" x2="${iconSize*0.3}" y2="0" stroke="white" stroke-width="${iconSize*0.08}" stroke-linecap="round"/>
|
||||
<line x1="0" y1="${-iconSize*0.3}" x2="0" y2="${iconSize*0.3}" stroke="white" stroke-width="${iconSize*0.08}" stroke-linecap="round"/>
|
||||
|
||||
<!-- Diagonal lines -->
|
||||
<line x1="${-iconSize*0.21}" y1="${-iconSize*0.21}" x2="${iconSize*0.21}" y2="${iconSize*0.21}" stroke="white" stroke-width="${iconSize*0.06}" stroke-linecap="round"/>
|
||||
<line x1="${iconSize*0.21}" y1="${-iconSize*0.21}" x2="${-iconSize*0.21}" y2="${iconSize*0.21}" stroke="white" stroke-width="${iconSize*0.06}" stroke-linecap="round"/>
|
||||
|
||||
<!-- Small decorative elements -->
|
||||
<circle cx="0" cy="0" r="${iconSize*0.04}" fill="white"/>
|
||||
<circle cx="${iconSize*0.15}" cy="0" r="${iconSize*0.02}" fill="white"/>
|
||||
<circle cx="${-iconSize*0.15}" cy="0" r="${iconSize*0.02}" fill="white"/>
|
||||
<circle cx="0" cy="${iconSize*0.15}" r="${iconSize*0.02}" fill="white"/>
|
||||
<circle cx="0" cy="${-iconSize*0.15}" r="${iconSize*0.02}" fill="white"/>
|
||||
</g>
|
||||
|
||||
${isMaskable ? `<!-- Safe zone indicator (invisible) -->
|
||||
<circle cx="${size/2}" cy="${size/2}" r="${size*0.4}" fill="none" stroke="none"/>` : ''}
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert SVG to PNG using a simple method
|
||||
* Note: In a real project, you'd use a proper SVG to PNG converter
|
||||
*/
|
||||
function generatePNGPlaceholder(size, isMaskable = false) {
|
||||
// For this example, we'll create a simple data URL
|
||||
// In a real implementation, you'd use sharp, canvas, or similar
|
||||
const svgContent = generateSVG(size, isMaskable);
|
||||
return svgContent;
|
||||
}
|
||||
|
||||
// Generate all required icon sizes
|
||||
console.log('🎨 Generating PWA icons...');
|
||||
|
||||
// Generate regular icons
|
||||
for (const size of SIZES) {
|
||||
const svgContent = generateSVG(size, false);
|
||||
const filename = `icon-${size}.png`;
|
||||
const filepath = path.join(iconsDir, filename);
|
||||
|
||||
// For now, save as SVG (in real implementation, convert to PNG)
|
||||
const svgFilename = `icon-${size}.svg`;
|
||||
const svgFilepath = path.join(iconsDir, svgFilename);
|
||||
fs.writeFileSync(svgFilepath, svgContent);
|
||||
|
||||
console.log(`✅ Generated ${svgFilename}`);
|
||||
}
|
||||
|
||||
// Generate maskable icons
|
||||
for (const size of MASKABLE_SIZES) {
|
||||
const svgContent = generateSVG(size, true);
|
||||
const svgFilename = `icon-${size}-maskable.svg`;
|
||||
const svgFilepath = path.join(iconsDir, svgFilename);
|
||||
fs.writeFileSync(svgFilepath, svgContent);
|
||||
|
||||
console.log(`✅ Generated ${svgFilename} (maskable)`);
|
||||
}
|
||||
|
||||
// Create a simple favicon.ico reference
|
||||
const faviconSVG = generateSVG(32, false);
|
||||
fs.writeFileSync(path.join(iconsDir, 'favicon.svg'), faviconSVG);
|
||||
|
||||
console.log('🎉 Icon generation complete!');
|
||||
console.log('📁 Icons saved to:', iconsDir);
|
||||
console.log('');
|
||||
console.log('📝 Note: SVG icons are generated for development.');
|
||||
console.log(' For production, convert these to PNG using a tool like:');
|
||||
console.log(' - sharp (Node.js)');
|
||||
console.log(' - ImageMagick');
|
||||
console.log(' - Online converters');
|
||||
console.log('');
|
||||
console.log('💡 To use PNG icons, update the file extensions in manifest.json');
|
Loading…
Add table
Add a link
Reference in a new issue