feat: Add PWA assets and metadata for iOS Safari installation

- Generate app icons in multiple sizes (192x192, 512x512, 180x180, 32x32)
- Create favicon and apple-touch-icon SVG files
- Add comprehensive PWA meta tags in app layout
- Configure theme color (#3B82F6) and background color
- Update manifest.json with icon references (PNG and SVG)
- Create icon generation scripts (SVG and PNG conversion)
- Add browser-based PNG icon generator (HTML)
- Create comprehensive PWA testing guide
- Add icon setup documentation

Requirements: 4.1, 4.2
Task: 13. Add PWA assets and metadata
This commit is contained in:
2025-12-01 20:39:11 +00:00
parent 9726f3a9e1
commit 6cb9a593d4
13 changed files with 1014 additions and 39 deletions

View File

@@ -0,0 +1,59 @@
// SVG to PNG converter using sharp library
// Run: npm install --save-dev sharp
// Then: node scripts/convert-with-sharp.js
const fs = require('fs');
const path = require('path');
async function convertIcons() {
try {
const sharp = require('sharp');
console.log('Converting SVG icons to PNG using sharp...\n');
const publicDir = path.join(__dirname, '..', 'public');
const conversions = [
{ input: 'icon-192.svg', output: 'icon-192.png', size: 192 },
{ input: 'icon-512.svg', output: 'icon-512.png', size: 512 },
{ input: 'apple-touch-icon.svg', output: 'apple-touch-icon.png', size: 180 },
{ input: 'favicon.svg', output: 'favicon-32.png', size: 32 }
];
for (const conv of conversions) {
const inputPath = path.join(publicDir, conv.input);
const outputPath = path.join(publicDir, conv.output);
if (!fs.existsSync(inputPath)) {
console.log(`⚠️ ${conv.input} not found, skipping...`);
continue;
}
await sharp(inputPath)
.resize(conv.size, conv.size)
.png()
.toFile(outputPath);
console.log(`✓ Created ${conv.output} (${conv.size}x${conv.size})`);
}
console.log('\n✓ All PNG icons generated successfully!');
console.log('\nNext steps:');
console.log(' 1. Update manifest.json to reference PNG icons');
console.log(' 2. Update app/layout.tsx to use PNG apple-touch-icon');
console.log(' 3. Test "Add to Home Screen" on iOS Safari');
} catch (error) {
if (error.code === 'MODULE_NOT_FOUND') {
console.error('❌ Sharp is not installed.');
console.log('\nTo install sharp, run:');
console.log(' npm install --save-dev sharp');
console.log('\nOr use alternative conversion methods:');
console.log(' node scripts/svg-to-png.js');
} else {
console.error('Error converting icons:', error.message);
}
process.exit(1);
}
}
convertIcons();

View File

@@ -1,45 +1,78 @@
// Icon generation script
// This script creates placeholder icon files for the PWA
// In production, you should replace these with proper high-quality icons
// Icon generation script for PWA
// This script creates SVG icons and PNG versions using canvas (Node.js)
// For production PWA support, especially on iOS
const fs = require('fs');
const path = require('path');
// Create a simple data URL for a blue icon with a checkmark
function createIconDataURL(size) {
// Simple SVG icon
const svg = `
<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
<rect width="${size}" height="${size}" rx="${size * 0.25}" fill="#3B82F6"/>
<path d="M${size * 0.3} ${size * 0.5}L${size * 0.45} ${size * 0.65}L${size * 0.7} ${size * 0.35}"
stroke="white" stroke-width="${size * 0.08}" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
`;
return `data:image/svg+xml;base64,${Buffer.from(svg).toString('base64')}`;
}
// For now, we'll create SVG files that browsers can use
// Create SVG icon with proper structure
function createSVGIcon(size, outputPath) {
const cornerRadius = size * 0.25;
const checkStartX = size * 0.3;
const checkStartY = size * 0.5;
const checkMidX = size * 0.45;
const checkMidY = size * 0.65;
const checkEndX = size * 0.7;
const checkEndY = size * 0.35;
const strokeWidth = size * 0.08;
const line1X = size * 0.25;
const line1Y = size * 0.6;
const line1Width = size * 0.5;
const line1Height = size * 0.08;
const lineRadius = size * 0.02;
const line2X = size * 0.25;
const line2Y = size * 0.72;
const line2Width = size * 0.4;
const line2Height = size * 0.08;
const svg = `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
<rect width="${size}" height="${size}" rx="${size * 0.25}" fill="#3B82F6"/>
<path d="M${size * 0.3} ${size * 0.5}L${size * 0.45} ${size * 0.65}L${size * 0.7} ${size * 0.35}"
stroke="white" stroke-width="${size * 0.08}" stroke-linecap="round" stroke-linejoin="round"/>
<rect x="${size * 0.25}" y="${size * 0.6}" width="${size * 0.5}" height="${size * 0.08}" rx="${size * 0.02}" fill="white"/>
<rect x="${size * 0.25}" y="${size * 0.72}" width="${size * 0.4}" height="${size * 0.08}" rx="${size * 0.02}" fill="white"/>
<rect width="${size}" height="${size}" rx="${cornerRadius}" fill="#3B82F6"/>
<path d="M${checkStartX} ${checkStartY}L${checkMidX} ${checkMidY}L${checkEndX} ${checkEndY}"
stroke="white" stroke-width="${strokeWidth}" stroke-linecap="round" stroke-linejoin="round"/>
<rect x="${line1X}" y="${line1Y}" width="${line1Width}" height="${line1Height}" rx="${lineRadius}" fill="white"/>
<rect x="${line2X}" y="${line2Y}" width="${line2Width}" height="${line2Height}" rx="${lineRadius}" fill="white"/>
</svg>`;
fs.writeFileSync(outputPath, svg);
console.log(`Created ${outputPath}`);
console.log(`✓ Created ${path.basename(outputPath)}`);
return svg;
}
// Create icons
// Create a simple favicon SVG
function createFaviconSVG(outputPath) {
const svg = `<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" rx="8" fill="#3B82F6"/>
<path d="M9.6 16L14.4 20.8L22.4 11.2"
stroke="white" stroke-width="2.56" stroke-linecap="round" stroke-linejoin="round"/>
<rect x="8" y="19.2" width="16" height="2.56" rx="0.64" fill="white"/>
<rect x="8" y="23.04" width="12.8" height="2.56" rx="0.64" fill="white"/>
</svg>`;
fs.writeFileSync(outputPath, svg);
console.log(`✓ Created ${path.basename(outputPath)}`);
}
// Main execution
const publicDir = path.join(__dirname, '..', 'public');
console.log('Generating PWA icons...\n');
// Create SVG icons
createSVGIcon(192, path.join(publicDir, 'icon-192.svg'));
createSVGIcon(512, path.join(publicDir, 'icon-512.svg'));
createSVGIcon(180, path.join(publicDir, 'apple-touch-icon.svg'));
createFaviconSVG(path.join(publicDir, 'favicon.svg'));
console.log('\nNote: SVG icons created. For production, convert these to PNG using:');
console.log(' - Online tools like https://cloudconvert.com/svg-to-png');
console.log(' - ImageMagick: convert icon-192.svg icon-192.png');
console.log(' - Or use a design tool like Figma/Sketch to export PNG versions');
console.log('\n✓ All SVG icons generated successfully!');
console.log('\nFor optimal PWA support (especially iOS), convert SVGs to PNG:');
console.log(' 1. Using online tools: https://cloudconvert.com/svg-to-png');
console.log(' 2. Using ImageMagick: convert icon-192.svg icon-192.png');
console.log(' 3. Using design tools: Figma, Sketch, or Adobe Illustrator');
console.log('\nRequired PNG files:');
console.log(' - icon-192.png (192x192)');
console.log(' - icon-512.png (512x512)');
console.log(' - apple-touch-icon.png (180x180)');
console.log(' - favicon.ico (32x32, multi-size)');
console.log('\nNote: iOS Safari requires PNG format for apple-touch-icon.');

View File

@@ -0,0 +1,226 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PWA Icon Generator</title>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
max-width: 800px;
margin: 40px auto;
padding: 20px;
background: #f5f5f5;
}
.container {
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
h1 {
color: #3B82F6;
margin-top: 0;
}
.icon-preview {
display: flex;
gap: 20px;
margin: 20px 0;
flex-wrap: wrap;
}
.icon-item {
text-align: center;
}
canvas {
border: 1px solid #ddd;
border-radius: 4px;
display: block;
margin: 10px 0;
}
button {
background: #3B82F6;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
margin: 5px;
}
button:hover {
background: #2563EB;
}
.instructions {
background: #f0f9ff;
border-left: 4px solid #3B82F6;
padding: 15px;
margin: 20px 0;
}
.success {
color: #059669;
font-weight: 500;
}
</style>
</head>
<body>
<div class="container">
<h1>🎨 Ultimate Lists PWA Icon Generator</h1>
<div class="instructions">
<strong>Instructions:</strong>
<ol>
<li>Click "Generate All Icons" below</li>
<li>Click each "Download" button to save the PNG files</li>
<li>Save the files to your <code>public/</code> directory with the exact names shown</li>
<li>Refresh your PWA to see the new icons</li>
</ol>
</div>
<button onclick="generateAllIcons()" style="font-size: 16px; padding: 12px 24px;">
Generate All Icons
</button>
<div id="icons" class="icon-preview"></div>
</div>
<script>
function drawIcon(canvas, size) {
const ctx = canvas.getContext('2d');
const scale = size / 512;
// Clear canvas
ctx.clearRect(0, 0, size, size);
// Draw rounded rectangle background
const cornerRadius = size * 0.25;
ctx.fillStyle = '#3B82F6';
ctx.beginPath();
ctx.roundRect(0, 0, size, size, cornerRadius);
ctx.fill();
// Draw checkmark
ctx.strokeStyle = 'white';
ctx.lineWidth = size * 0.08;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.beginPath();
ctx.moveTo(size * 0.3, size * 0.5);
ctx.lineTo(size * 0.45, size * 0.65);
ctx.lineTo(size * 0.7, size * 0.35);
ctx.stroke();
// Draw list lines
ctx.fillStyle = 'white';
// First line
const line1X = size * 0.25;
const line1Y = size * 0.6;
const line1Width = size * 0.5;
const line1Height = size * 0.08;
const lineRadius = size * 0.02;
ctx.beginPath();
ctx.roundRect(line1X, line1Y, line1Width, line1Height, lineRadius);
ctx.fill();
// Second line
const line2X = size * 0.25;
const line2Y = size * 0.72;
const line2Width = size * 0.4;
const line2Height = size * 0.08;
ctx.beginPath();
ctx.roundRect(line2X, line2Y, line2Width, line2Height, lineRadius);
ctx.fill();
}
function downloadCanvas(canvas, filename) {
const link = document.createElement('a');
link.download = filename;
link.href = canvas.toDataURL('image/png');
link.click();
}
function createIconElement(size, filename) {
const div = document.createElement('div');
div.className = 'icon-item';
const canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
drawIcon(canvas, size);
const label = document.createElement('div');
label.textContent = `${size}x${size}`;
label.style.fontWeight = '500';
label.style.marginBottom = '5px';
const filenameLabel = document.createElement('div');
filenameLabel.textContent = filename;
filenameLabel.style.fontSize = '12px';
filenameLabel.style.color = '#666';
filenameLabel.style.marginBottom = '10px';
const button = document.createElement('button');
button.textContent = 'Download';
button.onclick = () => {
downloadCanvas(canvas, filename);
button.textContent = '✓ Downloaded';
button.style.background = '#059669';
setTimeout(() => {
button.textContent = 'Download';
button.style.background = '#3B82F6';
}, 2000);
};
div.appendChild(label);
div.appendChild(filenameLabel);
div.appendChild(canvas);
div.appendChild(button);
return div;
}
function generateAllIcons() {
const iconsContainer = document.getElementById('icons');
iconsContainer.innerHTML = '';
const icons = [
{ size: 192, filename: 'icon-192.png' },
{ size: 512, filename: 'icon-512.png' },
{ size: 180, filename: 'apple-touch-icon.png' },
{ size: 32, filename: 'favicon-32.png' }
];
icons.forEach(icon => {
iconsContainer.appendChild(createIconElement(icon.size, icon.filename));
});
// Add download all button
const downloadAllBtn = document.createElement('button');
downloadAllBtn.textContent = 'Download All Icons';
downloadAllBtn.style.fontSize = '16px';
downloadAllBtn.style.padding = '12px 24px';
downloadAllBtn.style.marginTop = '20px';
downloadAllBtn.onclick = () => {
icons.forEach((icon, index) => {
setTimeout(() => {
const canvas = iconsContainer.querySelectorAll('canvas')[index];
downloadCanvas(canvas, icon.filename);
}, index * 500);
});
downloadAllBtn.textContent = '✓ All Icons Downloaded';
downloadAllBtn.style.background = '#059669';
};
iconsContainer.appendChild(downloadAllBtn);
}
// Auto-generate on load
window.onload = () => {
generateAllIcons();
};
</script>
</body>
</html>

47
scripts/svg-to-png.js Normal file
View File

@@ -0,0 +1,47 @@
// SVG to PNG converter using canvas (requires a browser environment or puppeteer)
// This is a simple converter for development purposes
const fs = require('fs');
const path = require('path');
console.log('SVG to PNG Converter');
console.log('===================\n');
const publicDir = path.join(__dirname, '..', 'public');
// Check if SVG files exist
const svgFiles = [
{ input: 'icon-192.svg', output: 'icon-192.png', size: 192 },
{ input: 'icon-512.svg', output: 'icon-512.png', size: 512 },
{ input: 'apple-touch-icon.svg', output: 'apple-touch-icon.png', size: 180 },
{ input: 'favicon.svg', output: 'favicon-32.png', size: 32 }
];
console.log('SVG files found:');
svgFiles.forEach(file => {
const exists = fs.existsSync(path.join(publicDir, file.input));
console.log(` ${exists ? '✓' : '✗'} ${file.input}`);
});
console.log('\n⚠️ PNG conversion requires additional tools:');
console.log('\nOption 1: Install sharp (recommended)');
console.log(' npm install --save-dev sharp');
console.log(' Then run: node scripts/convert-with-sharp.js');
console.log('\nOption 2: Use ImageMagick (if installed)');
console.log(' brew install imagemagick # on macOS');
console.log(' Then run:');
svgFiles.forEach(file => {
console.log(` convert public/${file.input} -resize ${file.size}x${file.size} public/${file.output}`);
});
console.log('\nOption 3: Use online converter');
console.log(' Visit: https://cloudconvert.com/svg-to-png');
console.log(' Upload SVG files and download PNG versions');
console.log('\nOption 4: Manual export from design tool');
console.log(' Open SVG files in Figma, Sketch, or Illustrator');
console.log(' Export as PNG at the required sizes');
console.log('\n📝 For now, the app will use SVG icons which work in most browsers.');
console.log(' PNG icons are recommended for better iOS Safari compatibility.');