Phase 6: Template System
- Added 8 pre-designed templates across 8 categories - Templates: Team Sport, Band Merch, Minimal Quote, Funny Cat, Gradient Vibes, Vintage Badge, Nature Lover, Tech Geek - Templates tab with category filter pills - Template preview cards with 2-column grid - One-click template application to canvas Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -66,11 +66,18 @@ function App() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleApplyTemplate = (template) => {
|
||||||
|
// Clear existing elements and apply template
|
||||||
|
template.elements.forEach((el, index) => {
|
||||||
|
setTimeout(() => addElement({ ...el }), index * 50);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-layout">
|
<div className="app-layout">
|
||||||
{/* Left Sidebar */}
|
{/* Left Sidebar */}
|
||||||
<aside className="sidebar-container">
|
<aside className="sidebar-container">
|
||||||
<Sidebar onElementAdd={handleAddElement} onUpload={handleUpload} />
|
<Sidebar onElementAdd={handleAddElement} onUpload={handleUpload} onApplyTemplate={handleApplyTemplate} />
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{/* Center Canvas */}
|
{/* Center Canvas */}
|
||||||
|
|||||||
@@ -2,14 +2,16 @@ import { useState } from 'react';
|
|||||||
import { UploadTab } from './UploadTab';
|
import { UploadTab } from './UploadTab';
|
||||||
import { StickersTab } from './StickersTab';
|
import { StickersTab } from './StickersTab';
|
||||||
import { TextTab } from './TextTab';
|
import { TextTab } from './TextTab';
|
||||||
|
import { TemplatesTab } from './TemplatesTab';
|
||||||
|
|
||||||
const TABS = [
|
const TABS = [
|
||||||
{ id: 'upload', label: 'Upload', icon: '📁' },
|
{ id: 'upload', label: 'Upload', icon: '📁' },
|
||||||
{ id: 'stickers', label: 'Stickers', icon: '😊' },
|
{ id: 'stickers', label: 'Stickers', icon: '😊' },
|
||||||
{ id: 'text', label: 'Text', icon: 'T' },
|
{ id: 'text', label: 'Text', icon: 'T' },
|
||||||
|
{ id: 'templates', label: 'Templates', icon: '📋' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function Sidebar({ onElementAdd, onUpload }) {
|
export function Sidebar({ onElementAdd, onUpload, onApplyTemplate }) {
|
||||||
const [activeTab, setActiveTab] = useState('upload');
|
const [activeTab, setActiveTab] = useState('upload');
|
||||||
|
|
||||||
const renderTabContent = () => {
|
const renderTabContent = () => {
|
||||||
@@ -20,6 +22,8 @@ export function Sidebar({ onElementAdd, onUpload }) {
|
|||||||
return <StickersTab onAddSticker={onElementAdd} />;
|
return <StickersTab onAddSticker={onElementAdd} />;
|
||||||
case 'text':
|
case 'text':
|
||||||
return <TextTab onAddText={onElementAdd} />;
|
return <TextTab onAddText={onElementAdd} />;
|
||||||
|
case 'templates':
|
||||||
|
return <TemplatesTab onApplyTemplate={onApplyTemplate} />;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
56
client/src/components/sidebar/TemplatesTab.jsx
Normal file
56
client/src/components/sidebar/TemplatesTab.jsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { TEMPLATES, TEMPLATE_CATEGORIES } from '../../constants/templates';
|
||||||
|
|
||||||
|
export function TemplatesTab({ onApplyTemplate }) {
|
||||||
|
const [selectedCategory, setSelectedCategory] = useState('All');
|
||||||
|
|
||||||
|
const filteredTemplates =
|
||||||
|
selectedCategory === 'All'
|
||||||
|
? TEMPLATES
|
||||||
|
: TEMPLATES.filter((t) => t.category === selectedCategory);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="templates-tab">
|
||||||
|
<h3>Templates</h3>
|
||||||
|
<div className="category-pills">
|
||||||
|
{TEMPLATE_CATEGORIES.map((cat) => (
|
||||||
|
<button
|
||||||
|
key={cat}
|
||||||
|
className={`category-pill ${selectedCategory === cat ? 'active' : ''}`}
|
||||||
|
onClick={() => setSelectedCategory(cat)}
|
||||||
|
>
|
||||||
|
{cat}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="templates-grid">
|
||||||
|
{filteredTemplates.map((template) => (
|
||||||
|
<button
|
||||||
|
key={template.id}
|
||||||
|
className="template-card"
|
||||||
|
onClick={() => onApplyTemplate(template)}
|
||||||
|
>
|
||||||
|
<div className="template-preview">
|
||||||
|
{template.elements.slice(0, 3).map((el, i) => (
|
||||||
|
<span
|
||||||
|
key={i}
|
||||||
|
className="template-preview-element"
|
||||||
|
style={{
|
||||||
|
fontSize: el.type === 'text' && el.text.length === 1 ? '16px' : '8px',
|
||||||
|
color: el.fill,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{el.text}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="template-info">
|
||||||
|
<span className="template-name">{template.name}</span>
|
||||||
|
<span className="template-category">{template.category}</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,3 +2,4 @@ export { Sidebar } from './Sidebar';
|
|||||||
export { UploadTab } from './UploadTab';
|
export { UploadTab } from './UploadTab';
|
||||||
export { StickersTab } from './StickersTab';
|
export { StickersTab } from './StickersTab';
|
||||||
export { TextTab } from './TextTab';
|
export { TextTab } from './TextTab';
|
||||||
|
export { TemplatesTab } from './TemplatesTab';
|
||||||
|
|||||||
309
client/src/constants/templates.js
Normal file
309
client/src/constants/templates.js
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
// Pre-designed templates for t-shirt customization
|
||||||
|
export const TEMPLATES = [
|
||||||
|
{
|
||||||
|
id: 'team-sport',
|
||||||
|
name: 'Team Sport',
|
||||||
|
category: 'Sports',
|
||||||
|
description: 'Classic team jersey with number and text',
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'TEAM NAME',
|
||||||
|
x: 75,
|
||||||
|
y: 80,
|
||||||
|
fontSize: 28,
|
||||||
|
fontFamily: 'Impact',
|
||||||
|
fill: '#ffffff',
|
||||||
|
rotation: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: '23',
|
||||||
|
x: 150,
|
||||||
|
y: 150,
|
||||||
|
fontSize: 72,
|
||||||
|
fontFamily: 'Impact',
|
||||||
|
fill: '#ffffff',
|
||||||
|
rotation: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'band-merch',
|
||||||
|
name: 'Band Merch',
|
||||||
|
category: 'Music',
|
||||||
|
description: 'Classic band t-shirt design',
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'BAND NAME',
|
||||||
|
x: 150,
|
||||||
|
y: 70,
|
||||||
|
fontSize: 32,
|
||||||
|
fontFamily: 'Georgia',
|
||||||
|
fill: '#fbbf24',
|
||||||
|
rotation: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'WORLD TOUR 2026',
|
||||||
|
x: 150,
|
||||||
|
y: 110,
|
||||||
|
fontSize: 16,
|
||||||
|
fontFamily: 'Arial',
|
||||||
|
fill: '#ffffff',
|
||||||
|
rotation: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: '🎸',
|
||||||
|
x: 150,
|
||||||
|
y: 180,
|
||||||
|
fontSize: 64,
|
||||||
|
fontFamily: 'Arial',
|
||||||
|
fill: '#ffffff',
|
||||||
|
rotation: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'minimal-quote',
|
||||||
|
name: 'Minimal Quote',
|
||||||
|
category: 'Quotes',
|
||||||
|
description: 'Simple centered quote design',
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: '"Be the change"',
|
||||||
|
x: 150,
|
||||||
|
y: 130,
|
||||||
|
fontSize: 24,
|
||||||
|
fontFamily: 'Georgia',
|
||||||
|
fill: '#1e293b',
|
||||||
|
rotation: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'you wish to see',
|
||||||
|
x: 150,
|
||||||
|
y: 160,
|
||||||
|
fontSize: 18,
|
||||||
|
fontFamily: 'Arial',
|
||||||
|
fill: '#64748b',
|
||||||
|
rotation: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'funny-cat',
|
||||||
|
name: 'Funny Cat',
|
||||||
|
category: 'Animals',
|
||||||
|
description: 'Cute cat with funny text',
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: '😼',
|
||||||
|
x: 150,
|
||||||
|
y: 100,
|
||||||
|
fontSize: 80,
|
||||||
|
fontFamily: 'Arial',
|
||||||
|
fill: '#000000',
|
||||||
|
rotation: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'I do what I want',
|
||||||
|
x: 150,
|
||||||
|
y: 200,
|
||||||
|
fontSize: 20,
|
||||||
|
fontFamily: 'Comic Sans MS',
|
||||||
|
fill: '#475569',
|
||||||
|
rotation: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gradient-vibes',
|
||||||
|
name: 'Gradient Vibes',
|
||||||
|
category: 'Abstract',
|
||||||
|
description: 'Modern gradient text design',
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'GOOD',
|
||||||
|
x: 150,
|
||||||
|
y: 110,
|
||||||
|
fontSize: 48,
|
||||||
|
fontFamily: 'Impact',
|
||||||
|
fill: '#ec4899',
|
||||||
|
rotation: -5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'VIBES',
|
||||||
|
x: 150,
|
||||||
|
y: 160,
|
||||||
|
fontSize: 48,
|
||||||
|
fontFamily: 'Impact',
|
||||||
|
fill: '#8b5cf6',
|
||||||
|
rotation: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: '✨',
|
||||||
|
x: 80,
|
||||||
|
y: 90,
|
||||||
|
fontSize: 32,
|
||||||
|
fontFamily: 'Arial',
|
||||||
|
fill: '#fbbf24',
|
||||||
|
rotation: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: '🌙',
|
||||||
|
x: 220,
|
||||||
|
y: 190,
|
||||||
|
fontSize: 32,
|
||||||
|
fontFamily: 'Arial',
|
||||||
|
fill: '#38bdf8',
|
||||||
|
rotation: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'vintage-badge',
|
||||||
|
name: 'Vintage Badge',
|
||||||
|
category: 'Vintage',
|
||||||
|
description: 'Retro badge style design',
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'EST.',
|
||||||
|
x: 150,
|
||||||
|
y: 80,
|
||||||
|
fontSize: 18,
|
||||||
|
fontFamily: 'Times New Roman',
|
||||||
|
fill: '#78716c',
|
||||||
|
rotation: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: '2026',
|
||||||
|
x: 150,
|
||||||
|
y: 105,
|
||||||
|
fontSize: 36,
|
||||||
|
fontFamily: 'Times New Roman',
|
||||||
|
fill: '#78716c',
|
||||||
|
rotation: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'AUTHENTIC',
|
||||||
|
x: 150,
|
||||||
|
y: 150,
|
||||||
|
fontSize: 24,
|
||||||
|
fontFamily: 'Times New Roman',
|
||||||
|
fill: '#78716c',
|
||||||
|
rotation: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'QUALITY',
|
||||||
|
x: 150,
|
||||||
|
y: 180,
|
||||||
|
fontSize: 24,
|
||||||
|
fontFamily: 'Times New Roman',
|
||||||
|
fill: '#78716c',
|
||||||
|
rotation: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'nature-lover',
|
||||||
|
name: 'Nature Lover',
|
||||||
|
category: 'Nature',
|
||||||
|
description: 'Mountain and nature themed',
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: '🏔️',
|
||||||
|
x: 150,
|
||||||
|
y: 90,
|
||||||
|
fontSize: 56,
|
||||||
|
fontFamily: 'Arial',
|
||||||
|
fill: '#000000',
|
||||||
|
rotation: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'ADVENTURE',
|
||||||
|
x: 150,
|
||||||
|
y: 160,
|
||||||
|
fontSize: 28,
|
||||||
|
fontFamily: 'Impact',
|
||||||
|
fill: '#059669',
|
||||||
|
rotation: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'AWAITS',
|
||||||
|
x: 150,
|
||||||
|
y: 190,
|
||||||
|
fontSize: 20,
|
||||||
|
fontFamily: 'Arial',
|
||||||
|
fill: '#6b7280',
|
||||||
|
rotation: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tech-geek',
|
||||||
|
name: 'Tech Geek',
|
||||||
|
category: 'Tech',
|
||||||
|
description: 'Programming themed design',
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: '</>',
|
||||||
|
x: 150,
|
||||||
|
y: 100,
|
||||||
|
fontSize: 64,
|
||||||
|
fontFamily: 'Courier New',
|
||||||
|
fill: '#3b82f6',
|
||||||
|
rotation: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'Hello, World!',
|
||||||
|
x: 150,
|
||||||
|
y: 170,
|
||||||
|
fontSize: 20,
|
||||||
|
fontFamily: 'Courier New',
|
||||||
|
fill: '#1e293b',
|
||||||
|
rotation: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: '// Code is life',
|
||||||
|
x: 150,
|
||||||
|
y: 195,
|
||||||
|
fontSize: 14,
|
||||||
|
fontFamily: 'Courier New',
|
||||||
|
fill: '#94a3b8',
|
||||||
|
rotation: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const TEMPLATE_CATEGORIES = [
|
||||||
|
'All',
|
||||||
|
'Sports',
|
||||||
|
'Music',
|
||||||
|
'Quotes',
|
||||||
|
'Animals',
|
||||||
|
'Abstract',
|
||||||
|
'Vintage',
|
||||||
|
'Nature',
|
||||||
|
'Tech',
|
||||||
|
];
|
||||||
@@ -389,6 +389,72 @@ input, textarea, select {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Templates Tab */
|
||||||
|
.templates-tab h3 {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin: 0 0 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.templates-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 0.75rem;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: var(--bg-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-card:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-preview {
|
||||||
|
height: 60px;
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-preview-element {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-name {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-category {
|
||||||
|
font-size: 0.625rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
/* Properties Panel */
|
/* Properties Panel */
|
||||||
.properties-panel {
|
.properties-panel {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
|||||||
Reference in New Issue
Block a user