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 (
|
||||
<div className="app-layout">
|
||||
{/* Left Sidebar */}
|
||||
<aside className="sidebar-container">
|
||||
<Sidebar onElementAdd={handleAddElement} onUpload={handleUpload} />
|
||||
<Sidebar onElementAdd={handleAddElement} onUpload={handleUpload} onApplyTemplate={handleApplyTemplate} />
|
||||
</aside>
|
||||
|
||||
{/* Center Canvas */}
|
||||
|
||||
@@ -2,14 +2,16 @@ 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: 'T' },
|
||||
{ id: 'templates', label: 'Templates', icon: '📋' },
|
||||
];
|
||||
|
||||
export function Sidebar({ onElementAdd, onUpload }) {
|
||||
export function Sidebar({ onElementAdd, onUpload, onApplyTemplate }) {
|
||||
const [activeTab, setActiveTab] = useState('upload');
|
||||
|
||||
const renderTabContent = () => {
|
||||
@@ -20,6 +22,8 @@ export function Sidebar({ onElementAdd, onUpload }) {
|
||||
return <StickersTab onAddSticker={onElementAdd} />;
|
||||
case 'text':
|
||||
return <TextTab onAddText={onElementAdd} />;
|
||||
case 'templates':
|
||||
return <TemplatesTab onApplyTemplate={onApplyTemplate} />;
|
||||
default:
|
||||
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 { StickersTab } from './StickersTab';
|
||||
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;
|
||||
}
|
||||
|
||||
/* 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 {
|
||||
padding: 1rem;
|
||||
|
||||
Reference in New Issue
Block a user