diff --git a/client/src/App.jsx b/client/src/App.jsx index 7bb8b82..dab0eb2 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -3,6 +3,7 @@ import { DesignCanvas } from './components/canvas/DesignCanvas'; import { Sidebar } from './components/sidebar/Sidebar'; import { PropertiesPanel } from './components/properties/PropertiesPanel'; import { useDesignEditor } from './hooks/useDesignEditor'; +import { useExport } from './hooks/useExport'; function App() { const { @@ -20,6 +21,8 @@ function App() { initializeHistory, } = useDesignEditor(); + const { exporting, progress, exportDesign, error, clearExport } = useExport(); + const selectedElement = elements.find((el) => el.id === selectedId); // Initialize history on mount @@ -116,25 +119,47 @@ function App() {

Apparel Designer

T-shirt customization editor

-
+
+
+ + +
-
+ {error && ( +
+

⚠️ Export failed: {error}

+ +
+ )}
{ + setExporting(true); + setProgress(0); + setError(null); + setExportUrl(null); + + try { + // Simulate progress during export + const progressInterval = setInterval(() => { + setProgress((prev) => Math.min(prev + 10, 90)); + }, 200); + + const response = await fetch('/api/export', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ elements, designName }), + }); + + clearInterval(progressInterval); + setProgress(100); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || 'Export failed'); + } + + const data = await response.json(); + setExportUrl(data.export.url); + + // Trigger download + const link = document.createElement('a'); + link.href = data.export.url; + link.download = data.export.filename; + link.click(); + + setExporting(false); + return data; + } catch (err) { + console.error('Export failed:', err); + setError(err.message); + setExporting(false); + throw err; + } + }, []); + + const clearExport = useCallback(() => { + setExportUrl(null); + setError(null); + }, []); + + return { + exporting, + progress, + exportUrl, + error, + exportDesign, + clearExport, + }; +} diff --git a/client/src/index.css b/client/src/index.css index e2513c5..de4e6b2 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -121,6 +121,12 @@ input, textarea, select { max-width: 400px; } +.canvas-actions { + display: flex; + gap: 0.75rem; + align-items: center; +} + .undo-redo-buttons { display: flex; gap: 0.5rem; @@ -153,6 +159,64 @@ input, textarea, select { cursor: not-allowed; } +.export-btn { + padding: 0.5rem 1rem; + background: linear-gradient(135deg, #22c55e, #16a34a); + color: white; + border: none; + border-radius: var(--radius-md); + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.export-btn:hover:not(:disabled) { + transform: translateY(-1px); + box-shadow: var(--shadow-md); +} + +.export-btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.export-error { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.75rem 1rem; + background: #fef2f2; + border: 1px solid #fecaca; + border-radius: var(--radius-md); + color: #dc2626; + font-size: 0.875rem; + margin-bottom: 1rem; + width: 100%; + max-width: 400px; +} + +.export-error p { + margin: 0; +} + +.close-error { + background: transparent; + border: none; + color: #dc2626; + cursor: pointer; + font-size: 1.25rem; + padding: 0; + line-height: 1; +} + +.close-error:hover { + color: #991b1b; +} + .canvas-wrapper { margin-bottom: 1rem; } diff --git a/server/index.js b/server/index.js index 08bf156..f0ca882 100644 --- a/server/index.js +++ b/server/index.js @@ -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, registerFont } 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,92 @@ app.use((err, req, res, next) => { next(err); }); +// High-resolution export endpoint +// Canvas: 300x300px preview -> 4500x4500px export (15"x15" @ 300 DPI) +const EXPORT_SCALE = 15; // 300px * 15 = 4500px +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' }); + } + + // Create high-resolution canvas + 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(); + + // Transform: translate to element position, rotate, then draw + 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 { + // Load image from URL or local path + 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`);