Phase 9: PWA & Workbox Caching

- Added vite-plugin-pwa and workbox-window dependencies
- PWA manifest with icons and standalone display mode
- Workbox runtime caching for:
  - Transformers.js models (30 days)
  - Uploaded images (7 days)
  - Google Fonts (1 year)
- PWAInstall component with install prompt banner
- Offline support for cached assets
- Auto-update registration

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Khalid A
2026-04-21 01:51:43 -05:00
parent 72a1967333
commit 5164b08c1c
7 changed files with 490 additions and 19 deletions

View File

@@ -3,9 +3,10 @@ import cors from 'cors';
import multer from 'multer';
import { v4 as uuidv4 } from 'uuid';
import sharp from 'sharp';
import { createCanvas, loadImage } from 'canvas';
import { fileURLToPath } from 'module';
import { dirname, join } from 'path';
import { mkdirSync, existsSync } from 'fs';
import { mkdirSync, existsSync, writeFileSync } from 'fs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
@@ -120,6 +121,86 @@ app.use((err, req, res, next) => {
next(err);
});
// High-resolution export endpoint (300x300px -> 4500x4500px @ 300 DPI)
const EXPORT_SCALE = 15;
const EXPORT_SIZE = 4500;
app.post('/api/export', async (req, res) => {
try {
const { elements, designName = 'design' } = req.body;
if (!elements || !Array.isArray(elements)) {
return res.status(400).json({ error: 'Elements array is required' });
}
const canvas = createCanvas(EXPORT_SIZE, EXPORT_SIZE);
const ctx = canvas.getContext('2d');
// White background
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, EXPORT_SIZE, EXPORT_SIZE);
// Render each element
for (const el of elements) {
ctx.save();
const x = (el.x || 0) * EXPORT_SCALE;
const y = (el.y || 0) * EXPORT_SCALE;
const centerX = x + ((el.width || el.fontSize || 100) * EXPORT_SCALE) / 2;
const centerY = y + ((el.height || el.fontSize || 100) * EXPORT_SCALE) / 2;
ctx.translate(centerX, centerY);
ctx.rotate((el.rotation || 0) * Math.PI / 180);
ctx.translate(-centerX, -centerY);
if (el.type === 'image' && el.src) {
try {
const imgUrl = el.src.startsWith('/')
? join(__dirname, el.src.replace('/uploads', 'uploads'))
: el.src;
const img = await loadImage(imgUrl);
const width = (el.width || 100) * EXPORT_SCALE;
const height = (el.height || 100) * EXPORT_SCALE;
ctx.drawImage(img, x, y, width, height);
} catch (imgError) {
console.error('Failed to load image for export:', imgError);
}
} else if (el.type === 'text') {
const fontSize = (el.fontSize || 32) * EXPORT_SCALE / 32;
ctx.font = `${fontSize}px "${el.fontFamily || 'Arial'}"`;
ctx.fillStyle = el.fill || '#000000';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(el.text || '', centerX, centerY);
}
ctx.restore();
}
// Save to file
const exportFilename = `${designName.replace(/[^a-z0-9]/gi, '_')}_${uuidv4()}.png`;
const exportPath = join(exportsDir, exportFilename);
const buffer = canvas.toBuffer('image/png');
writeFileSync(exportPath, buffer);
res.json({
success: true,
export: {
url: `/exports/${exportFilename}`,
path: exportPath,
filename: exportFilename,
width: EXPORT_SIZE,
height: EXPORT_SIZE,
dpi: 300,
sizeInches: '15x15'
}
});
} catch (error) {
console.error('Export error:', error);
res.status(500).json({ error: 'Failed to export design', details: error.message });
}
});
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
console.log(`Health check: http://localhost:${PORT}/api/health`);