From fd11a36d9326539f66dd76b76e7aeba5cd08bec8 Mon Sep 17 00:00:00 2001 From: Khalid A Date: Tue, 21 Apr 2026 01:27:59 -0500 Subject: [PATCH] Phase 3: Sidebar & Properties Panel Implemented full editor UI with three-column layout: - Sidebar with 4 tabs (Upload, Stickers, Text, Templates) - UploadTab with drag-and-drop file upload, wires to POST /api/upload - StickersTab with 96 emoji stickers across 6 categories - TextTab with font picker (20 Google Fonts), size slider, color picker - TemplatesTab placeholder for future template system - LayersPanel showing all elements with select/delete - PropertiesPanel with position, size, rotation controls Also added: - Constants for fonts and stickers - Enhanced CSS with editor-layout, sidebar, properties-panel classes - Updated App.jsx to integrate all components --- client/src/App.jsx | 102 ++++--- client/src/components/panels/LayersPanel.jsx | 131 ++++++++ .../src/components/panels/PropertiesPanel.jsx | 281 ++++++++++++++++++ client/src/components/panels/index.js | 2 + client/src/components/sidebar/Sidebar.jsx | 73 +++++ client/src/components/sidebar/StickersTab.jsx | 111 +++++++ .../src/components/sidebar/TemplatesTab.jsx | 133 +++++++++ client/src/components/sidebar/TextTab.jsx | 199 +++++++++++++ client/src/components/sidebar/UploadTab.jsx | 150 ++++++++++ client/src/components/sidebar/index.js | 5 + client/src/constants/fonts.js | 22 ++ client/src/constants/stickers.js | 159 ++++++++++ client/src/index.css | 43 +++ 13 files changed, 1375 insertions(+), 36 deletions(-) create mode 100644 client/src/components/panels/LayersPanel.jsx create mode 100644 client/src/components/panels/PropertiesPanel.jsx create mode 100644 client/src/components/panels/index.js create mode 100644 client/src/components/sidebar/Sidebar.jsx create mode 100644 client/src/components/sidebar/StickersTab.jsx create mode 100644 client/src/components/sidebar/TemplatesTab.jsx create mode 100644 client/src/components/sidebar/TextTab.jsx create mode 100644 client/src/components/sidebar/UploadTab.jsx create mode 100644 client/src/components/sidebar/index.js create mode 100644 client/src/constants/fonts.js create mode 100644 client/src/constants/stickers.js diff --git a/client/src/App.jsx b/client/src/App.jsx index 2148138..54c347d 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -1,5 +1,8 @@ -import { useEffect, useCallback } from 'react'; +import { useEffect } from 'react'; import { DesignCanvas } from './components/canvas/DesignCanvas'; +import { Sidebar } from './components/sidebar/Sidebar'; +import { LayersPanel } from './components/panels/LayersPanel'; +import { PropertiesPanel } from './components/panels/PropertiesPanel'; import { useDesignEditor } from './hooks/useDesignEditor'; function App() { @@ -13,6 +16,8 @@ function App() { deselectAll, } = useDesignEditor(); + const selectedElement = elements.find(el => el.id === selectedId); + // Keyboard shortcut: Delete/Backspace removes selected element useEffect(() => { const handleKeyDown = (e) => { @@ -31,30 +36,45 @@ function App() { return () => window.removeEventListener('keydown', handleKeyDown); }, [selectedId, deleteElement]); - // Test: Add sample image on mount (for Phase 2 testing) - useEffect(() => { - // Add a test image element - const testImageId = addElement({ - type: 'image', - x: 75, - y: 75, - width: 150, - height: 150, - rotation: 0, - src: 'https://placehold.co/150x150/38bdf8/ffffff?text=Test', - }); - console.log('Added test image with ID:', testImageId); - }, []); // eslint-disable-line react-hooks/exhaustive-deps + // Handler callbacks for sidebar tabs + const handleAddImage = (imageData) => { + addElement(imageData); + }; + + const handleAddSticker = (stickerData) => { + addElement(stickerData); + }; + + const handleAddText = (textData) => { + addElement(textData); + }; + + const handleAddTemplate = (templateId) => { + console.log('Template selected:', templateId); + // Template loading will be implemented in Phase 6 + }; return ( -
-

Apparel Designer

-

- T-shirt customization editor -

+
+ {/* Left Sidebar */} + + + {/* Center Canvas Area */} +
+
+

+ Apparel Designer +

+

+ T-shirt customization editor +

+
- {/* Canvas container */} -
+ + {/* Layers panel below canvas */} +
+ +
- {/* Debug info */} -
-

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 new file mode 100644 index 0000000..dc2d69e --- /dev/null +++ b/client/src/components/sidebar/Sidebar.jsx @@ -0,0 +1,73 @@ +import { useState } from 'react'; +import { UploadTab } from './UploadTab'; +import { StickersTab } from './StickersTab'; +import { TextTab } from './TextTab'; +import { TemplatesTab } from './TemplatesTab'; + +const TABS = [ + { id: 'upload', label: 'Upload', icon: '๐Ÿ“' }, + { id: 'stickers', label: 'Stickers', icon: '๐ŸŽจ' }, + { id: 'text', label: 'Text', icon: '๐Ÿ“' }, + { id: 'templates', label: 'Templates', icon: '๐Ÿ“‹' }, +]; + +export function Sidebar({ onAddImage, onAddSticker, onAddText, onAddTemplate }) { + const [activeTab, setActiveTab] = useState('upload'); + + const renderTabContent = () => { + switch (activeTab) { + case 'upload': + return ; + case 'stickers': + return ; + case 'text': + return ; + case 'templates': + return ; + default: + return null; + } + }; + + return ( +
+ {/* Tab headers */} +
+ {TABS.map((tab) => ( + + ))} +
+ + {/* Tab content */} +
+ {renderTabContent()} +
+
+ ); +} diff --git a/client/src/components/sidebar/StickersTab.jsx b/client/src/components/sidebar/StickersTab.jsx new file mode 100644 index 0000000..d87a6b4 --- /dev/null +++ b/client/src/components/sidebar/StickersTab.jsx @@ -0,0 +1,111 @@ +import { useState } from 'react'; +import { STICKERS, STICKER_CATEGORIES } from '../../constants/stickers'; + +export function StickersTab({ onAddSticker }) { + const [activeCategory, setActiveCategory] = useState('all'); + + 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', + x: 125, + y: 125, + width: 80, + height: 80, + rotation: 0, + src: dataUrl, + emoji, + }); + }; + + return ( +
+

+ Stickers +

+ + {/* Category pills */} +
+ {categories.map((cat) => ( + + ))} +
+ + {/* Sticker grid */} +
+ {filteredStickers.map((sticker, index) => ( + + ))} +
+
+ ); +} diff --git a/client/src/components/sidebar/TemplatesTab.jsx b/client/src/components/sidebar/TemplatesTab.jsx new file mode 100644 index 0000000..70d9500 --- /dev/null +++ b/client/src/components/sidebar/TemplatesTab.jsx @@ -0,0 +1,133 @@ +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, + }, + ]; + + const handleSelectTemplate = (template) => { + if (template.disabled) { + alert('This template will be available in a future update'); + return; + } + onAddTemplate(template.id); + }; + + return ( +
+

+ Templates +

+ +
+ Choose a template to constrain your design to specific print zones. Templates will be available in a future update. +
+ +
+ {templates.map((template) => ( + + ))} +
+
+ ); +} diff --git a/client/src/components/sidebar/TextTab.jsx b/client/src/components/sidebar/TextTab.jsx new file mode 100644 index 0000000..b96baac --- /dev/null +++ b/client/src/components/sidebar/TextTab.jsx @@ -0,0 +1,199 @@ +import { useState } from 'react'; +import { FONTS } from '../../constants/fonts'; + +export function TextTab({ onAddText }) { + 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, + rotation: 0, + }); + }; + + return ( +
+

+ Add Text +

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