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:
59
scripts/convert-with-sharp.js
Normal file
59
scripts/convert-with-sharp.js
Normal 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();
|
||||
@@ -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.');
|
||||
|
||||
226
scripts/generate-png-icons.html
Normal file
226
scripts/generate-png-icons.html
Normal 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
47
scripts/svg-to-png.js
Normal 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.');
|
||||
Reference in New Issue
Block a user