diff --git a/client/src/App.jsx b/client/src/App.jsx index dab0eb2..df07da6 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -1,7 +1,8 @@ import { useEffect } from 'react'; import { DesignCanvas } from './components/canvas/DesignCanvas'; import { Sidebar } from './components/sidebar/Sidebar'; -import { PropertiesPanel } from './components/properties/PropertiesPanel'; +import { LayersPanel } from './components/panels/LayersPanel'; +import { PropertiesPanel } from './components/panels/PropertiesPanel'; import { useDesignEditor } from './hooks/useDesignEditor'; import { useExport } from './hooks/useExport'; @@ -66,124 +67,147 @@ function App() { return () => window.removeEventListener('keydown', handleKeyDown); }, [selectedId, deleteElement, undo, redo, canUndo, canRedo]); - const handleUpload = (data) => { - if (data.preview?.url) { - addElement({ - type: 'image', - x: 75, - y: 75, - width: 150, - height: 150, - rotation: 0, - src: data.preview.url, - }); - } + // Handler callbacks for sidebar tabs + const handleAddImage = (imageData) => { + addElement(imageData); }; - const handleAddElement = (elementData) => { - if (elementData.type === 'sticker') { - // Convert emoji sticker to a text-like element - addElement({ - type: 'text', - text: elementData.emoji, - x: elementData.x, - y: elementData.y, - fontSize: elementData.size, - fontFamily: 'Arial', - fill: '#000000', - rotation: elementData.rotation, - }); - } else if (elementData.type === 'text') { - addElement(elementData); - } + const handleAddSticker = (stickerData) => { + addElement(stickerData); }; - const handleApplyTemplate = (template) => { - // Clear existing elements and apply template - template.elements.forEach((el, index) => { - setTimeout(() => addElement({ ...el }), index * 50); - }); + const handleAddText = (textData) => { + addElement(textData); + }; + + const handleAddTemplate = (template) => { + // Apply template elements to canvas + if (template && template.elements) { + template.elements.forEach((el, index) => { + setTimeout(() => addElement({ ...el }), index * 50); + }); + } }; return ( -
+
{/* Left Sidebar */} - + - {/* Center Canvas */} -
-
-
-

Apparel Designer

-

T-shirt customization editor

-
-
-
- - -
- -
+ {/* Center Canvas Area */} +
+
+

+ Apparel Designer +

+

+ T-shirt customization editor +

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

⚠️ Export failed: {error}

- +
+ ⚠️ Export failed: {error} +
)} -
- updateElement(id, attrs)} + /> + + {/* Layers panel below canvas */} +
+
-
-

Elements: {elements.length}

-

Selected: {selectedId || 'None'}

-

Tip: Click to select, drag to move, use handles to resize. Press Delete to remove.

-
-
+
{/* Right Properties Panel */} - + updateElement(selectedId, attrs)} + onDelete={deleteElement} + />
); } diff --git a/client/src/components/panels/LayersPanel.jsx b/client/src/components/panels/LayersPanel.jsx new file mode 100644 index 0000000..bff5f82 --- /dev/null +++ b/client/src/components/panels/LayersPanel.jsx @@ -0,0 +1,131 @@ +export function LayersPanel({ elements, selectedId, onSelect, onDelete }) { + const getIcon = (element) => { + switch (element.type) { + case 'image': + return element.bgRemoved ? '🖼️' : '📷'; + case 'text': + return '📝'; + case 'sticker': + return '🎨'; + default: + return '📁'; + } + }; + + const getName = (element) => { + switch (element.type) { + case 'image': + return element.bgRemoved ? 'Image (BG ✓)' : 'Image'; + case 'text': + return element.text?.substring(0, 20) || 'Text'; + case 'sticker': + return 'Sticker'; + default: + return 'Element'; + } + }; + + if (elements.length === 0) { + return ( +
+ No elements yet. Add images, text, or stickers to your design. +
+ ); + } + + return ( +
+

+ Layers ({elements.length}) +

+ +
+ {elements.map((element, index) => ( +
onSelect(element.id)} + style={{ + display: 'flex', + alignItems: 'center', + gap: '0.5rem', + padding: '0.5rem 0.75rem', + background: selectedId === element.id ? 'var(--accent-bg)' : 'transparent', + border: `1px solid ${selectedId === element.id ? 'var(--accent)' : 'var(--border)'}`, + borderRadius: 'var(--radius-sm)', + cursor: 'pointer', + transition: 'all 0.15s ease', + }} + onMouseEnter={(e) => { + if (selectedId !== element.id) { + e.target.style.borderColor = 'var(--accent)'; + } + }} + onMouseLeave={(e) => { + if (selectedId !== element.id) { + e.target.style.borderColor = 'var(--border)'; + } + }} + > + {getIcon(element)} + + {getName(element)} + + +
+ ))} +
+
+ ); +} diff --git a/client/src/components/panels/PropertiesPanel.jsx b/client/src/components/panels/PropertiesPanel.jsx new file mode 100644 index 0000000..316ffd6 --- /dev/null +++ b/client/src/components/panels/PropertiesPanel.jsx @@ -0,0 +1,281 @@ +export function PropertiesPanel({ element, onUpdate, onDelete }) { + if (!element) { + return ( +
+
+

+ Properties +

+
+ +
+ Select an element to edit its properties +
+
+ ); + } + + const handlePositionChange = (axis, value) => { + onUpdate({ [axis]: parseFloat(value) || 0 }); + }; + + const handleSizeChange = (axis, value) => { + const numValue = parseFloat(value) || 20; + onUpdate({ [axis]: Math.max(20, numValue) }); + }; + + const handleRotationChange = (value) => { + const numValue = parseFloat(value) || 0; + onUpdate({ rotation: Math.max(-180, Math.min(180, numValue)) }); + }; + + return ( +
+
+

+ Properties +

+
+ +
+ {/* Element type badge */} +
+ {element.type} +
+ + {/* Position */} +
+ +
+
+ + handlePositionChange('x', e.target.value)} + style={{ + width: '100%', + padding: '0.5rem', + border: `1px solid var(--border)`, + borderRadius: 'var(--radius-sm)', + fontSize: '13px', + }} + /> +
+
+ + handlePositionChange('y', e.target.value)} + style={{ + width: '100%', + padding: '0.5rem', + border: `1px solid var(--border)`, + borderRadius: 'var(--radius-sm)', + fontSize: '13px', + }} + /> +
+
+
+ + {/* Size (for images and stickers) */} + {(element.type === 'image' || element.type === 'sticker') && ( +
+ +
+
+ + handleSizeChange('width', e.target.value)} + style={{ + width: '100%', + padding: '0.5rem', + border: `1px solid var(--border)`, + borderRadius: 'var(--radius-sm)', + fontSize: '13px', + }} + /> +
+
+ + handleSizeChange('height', e.target.value)} + style={{ + width: '100%', + padding: '0.5rem', + border: `1px solid var(--border)`, + borderRadius: 'var(--radius-sm)', + fontSize: '13px', + }} + /> +
+
+
+ )} + + {/* Font size (for text) */} + {element.type === 'text' && ( + <> +
+ + onUpdate({ fontSize: parseInt(e.target.value, 10) })} + style={{ width: '100%' }} + /> +
+ +
+ + onUpdate({ fill: e.target.value })} + style={{ + width: '100%', + height: '36px', + border: `1px solid var(--border)`, + borderRadius: 'var(--radius-sm)', + cursor: 'pointer', + padding: '2px', + }} + /> +
+ + )} + + {/* Rotation */} +
+ + handleRotationChange(e.target.value)} + style={{ width: '100%' }} + /> +
+ + {/* Delete button */} + +
+
+ ); +} diff --git a/client/src/components/panels/index.js b/client/src/components/panels/index.js new file mode 100644 index 0000000..60db172 --- /dev/null +++ b/client/src/components/panels/index.js @@ -0,0 +1,2 @@ +export { LayersPanel } from './LayersPanel'; +export { PropertiesPanel } from './PropertiesPanel'; diff --git a/client/src/components/sidebar/Sidebar.jsx b/client/src/components/sidebar/Sidebar.jsx index 80dd71d..dc2d69e 100644 --- a/client/src/components/sidebar/Sidebar.jsx +++ b/client/src/components/sidebar/Sidebar.jsx @@ -6,24 +6,24 @@ import { TemplatesTab } from './TemplatesTab'; const TABS = [ { id: 'upload', label: 'Upload', icon: '📁' }, - { id: 'stickers', label: 'Stickers', icon: '😊' }, - { id: 'text', label: 'Text', icon: 'T' }, + { id: 'stickers', label: 'Stickers', icon: '🎨' }, + { id: 'text', label: 'Text', icon: '📝' }, { id: 'templates', label: 'Templates', icon: '📋' }, ]; -export function Sidebar({ onElementAdd, onUpload, onApplyTemplate }) { +export function Sidebar({ onAddImage, onAddSticker, onAddText, onAddTemplate }) { const [activeTab, setActiveTab] = useState('upload'); const renderTabContent = () => { switch (activeTab) { case 'upload': - return ; + return ; case 'stickers': - return ; + return ; case 'text': - return ; + return ; case 'templates': - return ; + return ; default: return null; } @@ -31,20 +31,43 @@ export function Sidebar({ onElementAdd, onUpload, onApplyTemplate }) { return (
-
+ {/* Tab headers */} +
{TABS.map((tab) => ( ))}
-
{renderTabContent()}
+ + {/* Tab content */} +
+ {renderTabContent()} +
); } diff --git a/client/src/components/sidebar/StickersTab.jsx b/client/src/components/sidebar/StickersTab.jsx index d6d4c29..d87a6b4 100644 --- a/client/src/components/sidebar/StickersTab.jsx +++ b/client/src/components/sidebar/StickersTab.jsx @@ -1,60 +1,108 @@ import { useState } from 'react'; - -const STICKER_CATEGORIES = [ - { id: 'all', label: 'All', emojis: [] }, - { id: 'faces', label: 'Faces', emojis: ['😀', '😁', '😂', '🤣', '😃', '😄', '😅', '😆', '😉', '😊', '😋', '😎', '😍', '😘', '🥰', '😗', '😙', '😚'] }, - { id: 'animals', label: 'Animals', emojis: ['🐶', '🐱', '🐭', '🐹', '🐰', '🦊', '🐻', '🐼', '🐨', '🐯', '🦁', '🐮', '🐷', '🐸', '🐵', '🐔', '🐧', '🐦', '🦆', '🦅'] }, - { id: 'food', label: 'Food', emojis: ['🍎', '🍐', '🍊', '🍋', '🍌', '🍉', '🍇', '🍓', '🍈', '🍒', '🍑', '🥭', '🍍', '🥥', '🥝', '🍅', '🍆', '🥑', '🥦', '🥬'] }, - { id: 'sports', label: 'Sports', emojis: ['⚽', '🏀', '🏈', '⚾', '🥎', '🎾', '🏐', '🏉', '🥏', '🎱', '🪀', '🏓', '🏸', '🏒', '🏑', '🥍', '🏏', '🥅', '⛳', '🪁'] }, - { id: 'symbols', label: 'Symbols', emojis: ['❤️', '🧡', '💛', '💚', '💙', '💜', '🖤', '🤍', '🤎', '💔', '❣️', '💕', '💞', '💓', '💗', '💖', '💘', '💝', '💟', '☮️'] }, - { id: 'objects', label: 'Objects', emojis: ['⌚', '📱', '💻', '⌨️', '🖥️', '🖨️', '🖱️', '🖲️', '🕹️', '🗜️', '💽', '💾', '💿', '📀', '📼', '📷', '📸', '📹', '🎥', '📽️'] }, -]; +import { STICKERS, STICKER_CATEGORIES } from '../../constants/stickers'; export function StickersTab({ onAddSticker }) { - const [selectedCategory, setSelectedCategory] = useState('faces'); + const [activeCategory, setActiveCategory] = useState('all'); - const getStickers = () => { - if (selectedCategory === 'all') { - return STICKER_CATEGORIES.flatMap((cat) => cat.emojis).filter(Boolean); - } - const category = STICKER_CATEGORIES.find((cat) => cat.id === selectedCategory); - return category?.emojis || []; - }; + const categories = ['all', ...STICKER_CATEGORIES]; + + const filteredStickers = activeCategory === 'all' + ? STICKERS + : STICKERS.filter(s => s.category === activeCategory); const handleAddSticker = (emoji) => { + // Create a canvas element with the emoji + const canvas = document.createElement('canvas'); + const size = 100; + canvas.width = size; + canvas.height = size; + const ctx = canvas.getContext('2d'); + + ctx.font = `${size * 0.8}px Arial`; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(emoji, size / 2, size / 2); + + const dataUrl = canvas.toDataURL('image/png'); + onAddSticker({ type: 'sticker', - emoji, - x: 100, - y: 100, - size: 64, + x: 125, + y: 125, + width: 80, + height: 80, rotation: 0, + src: dataUrl, + emoji, }); }; return ( -
-

Stickers

-
- {STICKER_CATEGORIES.map((cat) => ( +
+

+ Stickers +

+ + {/* Category pills */} +
+ {categories.map((cat) => ( ))}
-
- {getStickers().map((emoji, index) => ( + + {/* Sticker grid */} +
+ {filteredStickers.map((sticker, index) => ( ))}
diff --git a/client/src/components/sidebar/TemplatesTab.jsx b/client/src/components/sidebar/TemplatesTab.jsx index 7a3e913..70d9500 100644 --- a/client/src/components/sidebar/TemplatesTab.jsx +++ b/client/src/components/sidebar/TemplatesTab.jsx @@ -1,53 +1,130 @@ -import { useState } from 'react'; -import { TEMPLATES, TEMPLATE_CATEGORIES } from '../../constants/templates'; +export function TemplatesTab({ onAddTemplate }) { + const templates = [ + { + id: 'freeform', + name: 'Freeform', + description: 'No template - design freely', + thumbnail: '🎨', + }, + // Placeholder for future templates + { + id: 'classic-tee-front', + name: 'Classic Tee - Front', + description: 'Standard front chest print', + thumbnail: '👕', + disabled: true, + }, + { + id: 'classic-tee-back', + name: 'Classic Tee - Back', + description: 'Full back print', + thumbnail: '👕', + disabled: true, + }, + { + id: 'all-over', + name: 'All-Over Print', + description: 'Full front coverage', + thumbnail: '🎯', + disabled: true, + }, + ]; -export function TemplatesTab({ onApplyTemplate }) { - const [selectedCategory, setSelectedCategory] = useState('All'); - - const filteredTemplates = - selectedCategory === 'All' - ? TEMPLATES - : TEMPLATES.filter((t) => t.category === selectedCategory); + const handleSelectTemplate = (template) => { + if (template.disabled) { + alert('This template will be available in a future update'); + return; + } + onAddTemplate(template.id); + }; return ( -
-

Templates

-
- {TEMPLATE_CATEGORIES.map((cat) => ( - - ))} +
+

+ Templates +

+ +
+ Choose a template to constrain your design to specific print zones. Templates will be available in a future update.
-
- {filteredTemplates.map((template) => ( + +
+ {templates.map((template) => ( ))}
diff --git a/client/src/components/sidebar/TextTab.jsx b/client/src/components/sidebar/TextTab.jsx index b80ce44..b96baac 100644 --- a/client/src/components/sidebar/TextTab.jsx +++ b/client/src/components/sidebar/TextTab.jsx @@ -1,96 +1,199 @@ import { useState } from 'react'; - -const FONTS = [ - { value: 'Arial', label: 'Arial' }, - { value: 'Helvetica', label: 'Helvetica' }, - { value: 'Times New Roman', label: 'Times New Roman' }, - { value: 'Georgia', label: 'Georgia' }, - { value: 'Verdana', label: 'Verdana' }, - { value: 'Courier New', label: 'Courier New' }, - { value: 'Comic Sans MS', label: 'Comic Sans MS' }, - { value: 'Impact', label: 'Impact' }, -]; +import { FONTS } from '../../constants/fonts'; export function TextTab({ onAddText }) { - const [text, setText] = useState('Your Text'); - const [fontFamily, setFontFamily] = useState('Arial'); - const [fontSize, setFontSize] = useState(32); - const [fill, setFill] = useState('#000000'); + const [text, setText] = useState('Your text here'); + const [fontFamily, setFontFamily] = useState('Roboto'); + const [fontSize, setFontSize] = useState(48); + const [fill, setFill] = useState('#0f172a'); const handleAddText = () => { onAddText({ type: 'text', + x: 150, + y: 150, text, fontFamily, fontSize, fill, - x: 100, - y: 100, rotation: 0, }); }; return ( -
-

Add Text

-
- - +

+ Add Text +

+ + {/* Text input */} +
+ +