diff --git a/client/src/App.jsx b/client/src/App.jsx
index 2148138..f21be5b 100644
--- a/client/src/App.jsx
+++ b/client/src/App.jsx
@@ -1,5 +1,7 @@
-import { useEffect, useCallback } from 'react';
+import { useEffect } from 'react';
import { DesignCanvas } from './components/canvas/DesignCanvas';
+import { Sidebar } from './components/sidebar/Sidebar';
+import { PropertiesPanel } from './components/properties/PropertiesPanel';
import { useDesignEditor } from './hooks/useDesignEditor';
function App() {
@@ -13,10 +15,11 @@ function App() {
deselectAll,
} = useDesignEditor();
+ const selectedElement = elements.find((el) => el.id === selectedId);
+
// Keyboard shortcut: Delete/Backspace removes selected element
useEffect(() => {
const handleKeyDown = (e) => {
- // Don't delete if user is typing in an input
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
return;
}
@@ -31,53 +34,73 @@ 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
+ const handleUpload = (data) => {
+ if (data.preview?.url) {
+ addElement({
+ type: 'image',
+ x: 75,
+ y: 75,
+ width: 150,
+ height: 150,
+ rotation: 0,
+ src: data.preview.url,
+ });
+ }
+ };
+
+ 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);
+ }
+ };
return (
-
-
Apparel Designer
-
- T-shirt customization editor
-
+
+ {/* Left Sidebar */}
+
- {/* Canvas container */}
-
-
+ Apparel Designer
+ T-shirt customization editor
+
+
+
+
+
Elements: {elements.length}
+
Selected: {selectedId || 'None'}
+
Tip: Click to select, drag to move, use handles to resize. Press Delete to remove.
+
+
+
+ {/* Right Properties Panel */}
+
-
- {/* Debug info */}
-
-
Elements: {elements.length}
-
Selected: {selectedId || 'None'}
-
- Tip: Click to select, drag to move, use handles to resize. Press Delete to remove.
-
-
+
);
}
diff --git a/client/src/components/properties/PropertiesPanel.jsx b/client/src/components/properties/PropertiesPanel.jsx
new file mode 100644
index 0000000..7b9140e
--- /dev/null
+++ b/client/src/components/properties/PropertiesPanel.jsx
@@ -0,0 +1,109 @@
+export function PropertiesPanel({ selectedElement, onUpdate, onDelete }) {
+ if (!selectedElement) {
+ return (
+
+
Properties
+
+
Select an element to edit its properties
+
+
+ );
+ }
+
+ const handlePositionChange = (axis, value) => {
+ onUpdate(selectedElement.id, { [axis]: Number(value) });
+ };
+
+ const handleSizeChange = (dimension, value) => {
+ onUpdate(selectedElement.id, { [dimension]: Number(value) });
+ };
+
+ const handleRotationChange = (value) => {
+ onUpdate(selectedElement.id, { rotation: Number(value) });
+ };
+
+ const getIcon = () => {
+ if (selectedElement.type === 'image') return '๐ผ๏ธ';
+ if (selectedElement.type === 'text') return 'T';
+ if (selectedElement.type === 'sticker') return '๐';
+ return '๐ฆ';
+ };
+
+ return (
+
+
Properties
+
+ {getIcon()}
+
+ {selectedElement.type === 'text'
+ ? selectedElement.text?.substring(0, 20) || 'Text'
+ : `${selectedElement.type}`}
+
+
+
+
+
+
+
+
+
+ handleRotationChange(e.target.value)}
+ className="rotation-slider"
+ />
+
+
+
+
+ );
+}
diff --git a/client/src/components/properties/index.js b/client/src/components/properties/index.js
new file mode 100644
index 0000000..43c7b2c
--- /dev/null
+++ b/client/src/components/properties/index.js
@@ -0,0 +1 @@
+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..0f5e806
--- /dev/null
+++ b/client/src/components/sidebar/Sidebar.jsx
@@ -0,0 +1,46 @@
+import { useState } from 'react';
+import { UploadTab } from './UploadTab';
+import { StickersTab } from './StickersTab';
+import { TextTab } from './TextTab';
+
+const TABS = [
+ { id: 'upload', label: 'Upload', icon: '๐' },
+ { id: 'stickers', label: 'Stickers', icon: '๐' },
+ { id: 'text', label: 'Text', icon: 'T' },
+];
+
+export function Sidebar({ onElementAdd, onUpload }) {
+ const [activeTab, setActiveTab] = useState('upload');
+
+ const renderTabContent = () => {
+ switch (activeTab) {
+ case 'upload':
+ return
;
+ case 'stickers':
+ return
;
+ case 'text':
+ return
;
+ default:
+ return null;
+ }
+ };
+
+ return (
+
+
+ {TABS.map((tab) => (
+
+ ))}
+
+
{renderTabContent()}
+
+ );
+}
diff --git a/client/src/components/sidebar/StickersTab.jsx b/client/src/components/sidebar/StickersTab.jsx
new file mode 100644
index 0000000..d6d4c29
--- /dev/null
+++ b/client/src/components/sidebar/StickersTab.jsx
@@ -0,0 +1,63 @@
+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: ['โ', '๐ฑ', '๐ป', 'โจ๏ธ', '๐ฅ๏ธ', '๐จ๏ธ', '๐ฑ๏ธ', '๐ฒ๏ธ', '๐น๏ธ', '๐๏ธ', '๐ฝ', '๐พ', '๐ฟ', '๐', '๐ผ', '๐ท', '๐ธ', '๐น', '๐ฅ', '๐ฝ๏ธ'] },
+];
+
+export function StickersTab({ onAddSticker }) {
+ const [selectedCategory, setSelectedCategory] = useState('faces');
+
+ 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 handleAddSticker = (emoji) => {
+ onAddSticker({
+ type: 'sticker',
+ emoji,
+ x: 100,
+ y: 100,
+ size: 64,
+ rotation: 0,
+ });
+ };
+
+ return (
+
+
Stickers
+
+ {STICKER_CATEGORIES.map((cat) => (
+
+ ))}
+
+
+ {getStickers().map((emoji, index) => (
+
+ ))}
+
+
+ );
+}
diff --git a/client/src/components/sidebar/TextTab.jsx b/client/src/components/sidebar/TextTab.jsx
new file mode 100644
index 0000000..b80ce44
--- /dev/null
+++ b/client/src/components/sidebar/TextTab.jsx
@@ -0,0 +1,96 @@
+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' },
+];
+
+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 handleAddText = () => {
+ onAddText({
+ type: 'text',
+ text,
+ fontFamily,
+ fontSize,
+ fill,
+ x: 100,
+ y: 100,
+ rotation: 0,
+ });
+ };
+
+ return (
+
+
Add Text
+
+
+ setText(e.target.value)}
+ placeholder="Enter text"
+ className="text-input"
+ />
+
+
+
+
+
+
+
+ setFontSize(Number(e.target.value))}
+ className="size-slider"
+ />
+
+
+
+
+ {text}
+
+
+ );
+}
diff --git a/client/src/components/sidebar/UploadTab.jsx b/client/src/components/sidebar/UploadTab.jsx
new file mode 100644
index 0000000..51325eb
--- /dev/null
+++ b/client/src/components/sidebar/UploadTab.jsx
@@ -0,0 +1,80 @@
+import { useRef, useState } from 'react';
+
+export function UploadTab({ onUpload }) {
+ const fileInputRef = useRef(null);
+ const [isDragging, setIsDragging] = useState(false);
+ const [uploading, setUploading] = useState(false);
+
+ const handleFile = async (file) => {
+ if (!file || !file.type.startsWith('image/')) return;
+
+ setUploading(true);
+ const formData = new FormData();
+ formData.append('image', file);
+
+ try {
+ const response = await fetch('/api/upload', {
+ method: 'POST',
+ body: formData,
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ onUpload(data);
+ }
+ } catch (error) {
+ console.error('Upload failed:', error);
+ } finally {
+ setUploading(false);
+ }
+ };
+
+ const handleDrop = (e) => {
+ e.preventDefault();
+ setIsDragging(false);
+ const file = e.dataTransfer.files[0];
+ handleFile(file);
+ };
+
+ const handleDragOver = (e) => {
+ e.preventDefault();
+ setIsDragging(true);
+ };
+
+ const handleDragLeave = () => {
+ setIsDragging(false);
+ };
+
+ return (
+
+
Upload Image
+
fileInputRef.current?.click()}
+ >
+ {uploading ? (
+
+ ) : (
+ <>
+
๐
+
Drop image here or click to upload
+
PNG, JPG, WEBP up to 20MB
+ >
+ )}
+
handleFile(e.target.files[0])}
+ style={{ display: 'none' }}
+ />
+
+
+ );
+}
diff --git a/client/src/components/sidebar/index.js b/client/src/components/sidebar/index.js
new file mode 100644
index 0000000..216fdb5
--- /dev/null
+++ b/client/src/components/sidebar/index.js
@@ -0,0 +1,4 @@
+export { Sidebar } from './Sidebar';
+export { UploadTab } from './UploadTab';
+export { StickersTab } from './StickersTab';
+export { TextTab } from './TextTab';
diff --git a/client/src/index.css b/client/src/index.css
index fb6de52..1790f68 100644
--- a/client/src/index.css
+++ b/client/src/index.css
@@ -66,3 +66,448 @@ button {
input, textarea, select {
font-family: inherit;
}
+
+/* App Layout - Three Column */
+.app-layout {
+ display: flex;
+ min-height: 100vh;
+ background: var(--bg-secondary);
+}
+
+.sidebar-container {
+ width: 280px;
+ background: var(--bg-primary);
+ border-right: 1px solid var(--border);
+ flex-shrink: 0;
+ overflow-y: auto;
+}
+
+.canvas-container {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 2rem;
+ overflow-y: auto;
+}
+
+.properties-container {
+ width: 260px;
+ background: var(--bg-primary);
+ border-left: 1px solid var(--border);
+ flex-shrink: 0;
+ overflow-y: auto;
+}
+
+.app-title {
+ font-size: 1.5rem;
+ font-weight: 600;
+ margin: 0 0 0.5rem 0;
+ color: var(--text-primary);
+}
+
+.app-subtitle {
+ color: var(--text-secondary);
+ margin: 0 0 1.5rem 0;
+ font-size: 0.875rem;
+}
+
+.canvas-wrapper {
+ margin-bottom: 1rem;
+}
+
+.debug-info {
+ padding: 1rem;
+ background: var(--bg-tertiary);
+ border-radius: var(--radius-md);
+ font-size: 0.875rem;
+ text-align: center;
+}
+
+.debug-info p {
+ margin: 0.25rem 0;
+}
+
+.debug-info .tip {
+ color: var(--text-muted);
+ font-size: 12px;
+}
+
+/* Sidebar */
+.sidebar {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+}
+
+.sidebar-tabs {
+ display: flex;
+ border-bottom: 1px solid var(--border);
+ background: var(--bg-tertiary);
+}
+
+.sidebar-tab {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 0.75rem 0.5rem;
+ background: transparent;
+ border: none;
+ border-bottom: 2px solid transparent;
+ cursor: pointer;
+ transition: all 0.2s;
+ font-size: 0.75rem;
+ color: var(--text-secondary);
+}
+
+.sidebar-tab:hover {
+ background: var(--bg-primary);
+}
+
+.sidebar-tab.active {
+ border-bottom-color: var(--accent);
+ color: var(--text-primary);
+}
+
+.tab-icon {
+ font-size: 1.25rem;
+ margin-bottom: 0.25rem;
+}
+
+.tab-label {
+ font-size: 0.625rem;
+}
+
+.sidebar-content {
+ padding: 1rem;
+ flex: 1;
+}
+
+/* Upload Tab */
+.upload-tab h3 {
+ font-size: 0.875rem;
+ margin: 0 0 1rem 0;
+ color: var(--text-primary);
+}
+
+.upload-zone {
+ border: 2px dashed var(--border);
+ border-radius: var(--radius-md);
+ padding: 2rem 1rem;
+ text-align: center;
+ cursor: pointer;
+ transition: all 0.2s;
+ background: var(--bg-secondary);
+}
+
+.upload-zone:hover {
+ border-color: var(--accent);
+ background: var(--accent-bg);
+}
+
+.upload-zone.dragging {
+ border-color: var(--accent);
+ background: var(--accent-bg);
+}
+
+.upload-zone.uploading {
+ opacity: 0.7;
+ pointer-events: none;
+}
+
+.upload-icon {
+ font-size: 2.5rem;
+ margin-bottom: 0.5rem;
+}
+
+.upload-zone p {
+ margin: 0.25rem 0;
+ font-size: 0.875rem;
+ color: var(--text-secondary);
+}
+
+.upload-hint {
+ font-size: 0.75rem !important;
+ color: var(--text-muted) !important;
+}
+
+.uploading-state {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.spinner {
+ width: 24px;
+ height: 24px;
+ border: 2px solid var(--border);
+ border-top-color: var(--accent);
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+ to { transform: rotate(360deg); }
+}
+
+/* Stickers Tab */
+.stickers-tab h3 {
+ font-size: 0.875rem;
+ margin: 0 0 1rem 0;
+}
+
+.category-pills {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ margin-bottom: 1rem;
+}
+
+.category-pill {
+ padding: 0.25rem 0.75rem;
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ background: var(--bg-secondary);
+ font-size: 0.75rem;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+.category-pill:hover {
+ border-color: var(--accent);
+}
+
+.category-pill.active {
+ background: var(--accent);
+ border-color: var(--accent);
+ color: white;
+}
+
+.sticker-grid {
+ display: grid;
+ grid-template-columns: repeat(6, 1fr);
+ gap: 0.5rem;
+ max-height: 400px;
+ overflow-y: auto;
+}
+
+.sticker-button {
+ font-size: 1.5rem;
+ padding: 0.5rem;
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ background: var(--bg-primary);
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+.sticker-button:hover {
+ transform: scale(1.1);
+ border-color: var(--accent);
+}
+
+/* Text Tab */
+.text-tab h3 {
+ font-size: 0.875rem;
+ margin: 0 0 1rem 0;
+}
+
+.text-input-group {
+ margin-bottom: 1rem;
+}
+
+.text-input-group label {
+ display: block;
+ font-size: 0.75rem;
+ color: var(--text-secondary);
+ margin-bottom: 0.25rem;
+}
+
+.text-input,
+.font-select {
+ width: 100%;
+ padding: 0.5rem;
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ font-size: 0.875rem;
+}
+
+.text-input:focus,
+.font-select:focus {
+ outline: none;
+ border-color: var(--accent);
+}
+
+.size-slider {
+ width: 100%;
+}
+
+.color-picker-row {
+ display: flex;
+ gap: 0.5rem;
+}
+
+.color-picker {
+ width: 40px;
+ height: 36px;
+ padding: 0;
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ cursor: pointer;
+}
+
+.color-input {
+ flex: 1;
+ padding: 0.5rem;
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+}
+
+.add-text-btn {
+ width: 100%;
+ padding: 0.75rem;
+ background: var(--accent);
+ color: white;
+ border: none;
+ border-radius: var(--radius-md);
+ font-weight: 500;
+ cursor: pointer;
+ transition: background 0.2s;
+}
+
+.add-text-btn:hover {
+ background: var(--accent-hover);
+}
+
+.text-preview {
+ margin-top: 1rem;
+ padding: 1rem;
+ background: var(--bg-secondary);
+ border-radius: var(--radius-md);
+ text-align: center;
+}
+
+/* Properties Panel */
+.properties-panel {
+ padding: 1rem;
+}
+
+.properties-panel h3 {
+ font-size: 0.875rem;
+ margin: 0 0 1rem 0;
+ color: var(--text-primary);
+}
+
+.no-selection {
+ padding: 2rem 1rem;
+ text-align: center;
+ color: var(--text-muted);
+ font-size: 0.875rem;
+}
+
+.element-header {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.75rem;
+ background: var(--bg-tertiary);
+ border-radius: var(--radius-md);
+ margin-bottom: 1rem;
+}
+
+.element-icon {
+ font-size: 1.25rem;
+}
+
+.element-name {
+ font-weight: 500;
+ font-size: 0.875rem;
+}
+
+.property-group {
+ margin-bottom: 1rem;
+}
+
+.property-group label {
+ display: block;
+ font-size: 0.75rem;
+ color: var(--text-secondary);
+ margin-bottom: 0.5rem;
+}
+
+.property-row {
+ display: flex;
+ gap: 0.5rem;
+}
+
+.property-input {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.property-label {
+ font-size: 0.75rem;
+ color: var(--text-muted);
+ min-width: 12px;
+}
+
+.property-input input {
+ flex: 1;
+ padding: 0.5rem;
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ font-size: 0.875rem;
+ text-align: center;
+}
+
+.property-input input:focus {
+ outline: none;
+ border-color: var(--accent);
+}
+
+.rotation-slider {
+ width: 100%;
+}
+
+.delete-btn {
+ width: 100%;
+ padding: 0.75rem;
+ background: transparent;
+ color: var(--error);
+ border: 1px solid var(--error);
+ border-radius: var(--radius-md);
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+.delete-btn:hover {
+ background: var(--error);
+ color: white;
+}
+
+/* Responsive */
+@media (max-width: 900px) {
+ .app-layout {
+ flex-direction: column;
+ }
+
+ .sidebar-container,
+ .properties-container {
+ width: 100%;
+ border: none;
+ border-bottom: 1px solid var(--border);
+ }
+
+ .sidebar-tabs {
+ overflow-x: auto;
+ }
+
+ .sticker-grid {
+ grid-template-columns: repeat(8, 1fr);
+ }
+}