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`);