Complete Phase 5 and Phase 6 features
Phase 5 - Photo Pre-Editor: - Add Filerobot integration for image editing (crop, filters, adjustments) - Add BG removal prompt modal after Filerobot edit completes - Add 'Edit Photo' button in PropertiesPanel for re-editing images - Support re-edit flow with editingElementId state tracking Phase 6 - Template System: - Add confirmation dialog when switching templates with existing work - Add layers.background and layers.overlay to all 8 templates - Implement auto-crop for slotted images using object-fit: cover logic - Add Transformer boundBoxFunc to respect slot boundaries during resize - Pass elements prop to TemplatesTab for detecting existing canvas work Files modified: - App.jsx: BG removal prompt, photo edit handlers - ImageElement.jsx: crop property, slot-aware Transformer constraints - PropertiesPanel.jsx: Edit Photo button - TemplatesTab.jsx: confirmation dialog for template switch - templates.js: layers.background/overlay for all templates - Sidebar.jsx: pass elements prop to TemplatesTab - DesignCanvas.jsx: pass slot prop to ImageElement Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,9 @@ import { TEMPLATES } from './constants/templates';
|
|||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [editingElement, setEditingElement] = useState(null);
|
const [editingElement, setEditingElement] = useState(null);
|
||||||
|
const [pendingImage, setPendingImage] = useState(null);
|
||||||
|
const [showBgRemovalPrompt, setShowBgRemovalPrompt] = useState(false);
|
||||||
|
const [editingElementId, setEditingElementId] = useState(null);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
elements,
|
elements,
|
||||||
@@ -128,18 +131,50 @@ function App() {
|
|||||||
|
|
||||||
// Handle photo editing
|
// Handle photo editing
|
||||||
const handleEditPhoto = (element) => {
|
const handleEditPhoto = (element) => {
|
||||||
setEditingElement(element);
|
setEditingElementId(element.id);
|
||||||
|
setPendingImage({ src: element.src, id: element.id });
|
||||||
|
setShowBgRemovalPrompt(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePhotoEditComplete = (editedImageUrl) => {
|
const handlePhotoEditComplete = (editedImageUrl) => {
|
||||||
if (editingElement) {
|
setPendingImage(prev => ({ ...prev, editedUrl: editedImageUrl }));
|
||||||
updateElement(editingElement.id, { src: editedImageUrl });
|
setShowBgRemovalPrompt(true);
|
||||||
}
|
|
||||||
setEditingElement(null);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePhotoEditClose = () => {
|
const handlePhotoEditClose = () => {
|
||||||
setEditingElement(null);
|
setPendingImage(null);
|
||||||
|
setEditingElementId(null);
|
||||||
|
setShowBgRemovalPrompt(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBgRemovalYes = async () => {
|
||||||
|
if (pendingImage?.editedUrl) {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/remove-background', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ image: pendingImage.editedUrl }),
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.success && editingElementId) {
|
||||||
|
updateElement(editingElementId, { src: data.result });
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Background removal failed:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setShowBgRemovalPrompt(false);
|
||||||
|
setPendingImage(null);
|
||||||
|
setEditingElementId(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBgRemovalNo = () => {
|
||||||
|
if (pendingImage?.editedUrl && editingElementId) {
|
||||||
|
updateElement(editingElementId, { src: pendingImage.editedUrl });
|
||||||
|
}
|
||||||
|
setShowBgRemovalPrompt(false);
|
||||||
|
setPendingImage(null);
|
||||||
|
setEditingElementId(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -157,6 +192,7 @@ function App() {
|
|||||||
onAddText={handleAddText}
|
onAddText={handleAddText}
|
||||||
onAddTemplate={handleAddTemplate}
|
onAddTemplate={handleAddTemplate}
|
||||||
onSlotImageUpload={handleSlotImageUpload}
|
onSlotImageUpload={handleSlotImageUpload}
|
||||||
|
elements={elements}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Center Canvas Area */}
|
{/* Center Canvas Area */}
|
||||||
@@ -280,13 +316,86 @@ function App() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Photo Pre-Editor Modal */}
|
{/* Photo Pre-Editor Modal */}
|
||||||
{editingElement && (
|
{pendingImage && (
|
||||||
<PhotoPreEditor
|
<PhotoPreEditor
|
||||||
imageSrc={editingElement.src}
|
imageSrc={pendingImage.src}
|
||||||
onComplete={handlePhotoEditComplete}
|
onComplete={handlePhotoEditComplete}
|
||||||
onClose={handlePhotoEditClose}
|
onClose={handlePhotoEditClose}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Background removal prompt after Filerobot edit */}
|
||||||
|
{showBgRemovalPrompt && (
|
||||||
|
<div style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
background: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
zIndex: 1000,
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
background: 'var(--bg-primary)',
|
||||||
|
borderRadius: 'var(--radius-md)',
|
||||||
|
padding: '1.5rem',
|
||||||
|
maxWidth: '360px',
|
||||||
|
boxShadow: 'var(--shadow-lg)',
|
||||||
|
}}>
|
||||||
|
<h3 style={{
|
||||||
|
margin: '0 0 0.75rem 0',
|
||||||
|
fontSize: '16px',
|
||||||
|
fontWeight: '600',
|
||||||
|
color: 'var(--text-primary)',
|
||||||
|
}}>
|
||||||
|
Remove Background?
|
||||||
|
</h3>
|
||||||
|
<p style={{
|
||||||
|
margin: '0 0 1.25rem 0',
|
||||||
|
fontSize: '13px',
|
||||||
|
color: 'var(--text-secondary)',
|
||||||
|
lineHeight: '1.4',
|
||||||
|
}}>
|
||||||
|
AI can automatically remove the background from your image.
|
||||||
|
</p>
|
||||||
|
<div style={{ display: 'flex', gap: '0.5rem', justifyContent: 'flex-end' }}>
|
||||||
|
<button
|
||||||
|
onClick={handleBgRemovalNo}
|
||||||
|
style={{
|
||||||
|
padding: '0.5rem 1rem',
|
||||||
|
border: `1px solid var(--border)`,
|
||||||
|
borderRadius: 'var(--radius-sm)',
|
||||||
|
background: 'var(--bg-primary)',
|
||||||
|
color: 'var(--text-primary)',
|
||||||
|
fontSize: '13px',
|
||||||
|
fontWeight: 500,
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Skip
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleBgRemovalYes}
|
||||||
|
style={{
|
||||||
|
padding: '0.5rem 1rem',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: 'var(--radius-sm)',
|
||||||
|
background: 'var(--accent)',
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: '13px',
|
||||||
|
fontWeight: 600,
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
✨ Remove Background
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ export const DesignCanvas = memo(function DesignCanvas({
|
|||||||
<Layer>
|
<Layer>
|
||||||
{elements.map((el) => {
|
{elements.map((el) => {
|
||||||
if (el.type === 'image') {
|
if (el.type === 'image') {
|
||||||
|
const slot = el.slotId ? slots.find(s => s.id === el.slotId) : null;
|
||||||
return (
|
return (
|
||||||
<ImageElement
|
<ImageElement
|
||||||
key={el.id}
|
key={el.id}
|
||||||
@@ -71,7 +72,7 @@ export const DesignCanvas = memo(function DesignCanvas({
|
|||||||
height={el.height}
|
height={el.height}
|
||||||
rotation={el.rotation}
|
rotation={el.rotation}
|
||||||
src={el.src}
|
src={el.src}
|
||||||
crop={el.crop}
|
slot={slot}
|
||||||
isSelected={el.id === selectedId}
|
isSelected={el.id === selectedId}
|
||||||
onSelect={() => onSelect(el.id)}
|
onSelect={() => onSelect(el.id)}
|
||||||
onUpdate={(attrs) => onUpdate(el.id, attrs)}
|
onUpdate={(attrs) => onUpdate(el.id, attrs)}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import { useEffect, useState, memo } from 'react';
|
|||||||
import { Image, Transformer } from 'react-konva';
|
import { Image, Transformer } from 'react-konva';
|
||||||
import useImage from 'use-image';
|
import useImage from 'use-image';
|
||||||
|
|
||||||
function URLImage({ src, ...props }) {
|
function URLImage({ src, crop, ...props }) {
|
||||||
const [img] = useImage(src, 'anonymous');
|
const [img] = useImage(src, 'anonymous');
|
||||||
return <Image image={img} {...props} />;
|
return <Image image={img} crop={crop} {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ImageElement = memo(function ImageElement({
|
export const ImageElement = memo(function ImageElement({
|
||||||
@@ -15,20 +15,44 @@ export const ImageElement = memo(function ImageElement({
|
|||||||
height,
|
height,
|
||||||
rotation,
|
rotation,
|
||||||
src,
|
src,
|
||||||
|
slot,
|
||||||
isSelected,
|
isSelected,
|
||||||
onSelect,
|
onSelect,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
onCommit,
|
onCommit,
|
||||||
|
dragBoundFunc,
|
||||||
}) {
|
}) {
|
||||||
const shapeRef = null;
|
const shapeRef = null;
|
||||||
const trRef = null;
|
const trRef = null;
|
||||||
|
const [crop, setCrop] = useState(null);
|
||||||
|
|
||||||
|
// Calculate crop for slotted images using object-fit: cover logic
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isSelected && trRef.current) {
|
if (slot && src) {
|
||||||
trRef.current.nodes([shapeRef.current]);
|
const img = new Image();
|
||||||
trRef.current.getLayer().batchDraw();
|
img.src = src;
|
||||||
|
img.onload = () => {
|
||||||
|
const imageRatio = img.width / img.height;
|
||||||
|
const slotRatio = slot.bounds.width / slot.bounds.height;
|
||||||
|
|
||||||
|
let cropWidth, cropHeight, cropX = 0, cropY = 0;
|
||||||
|
|
||||||
|
if (imageRatio > slotRatio) {
|
||||||
|
// Image is wider than slot - crop width
|
||||||
|
cropHeight = img.height;
|
||||||
|
cropWidth = img.height * slotRatio;
|
||||||
|
cropX = (img.width - cropWidth) / 2;
|
||||||
|
} else {
|
||||||
|
// Image is taller than slot - crop height
|
||||||
|
cropWidth = img.width;
|
||||||
|
cropHeight = img.width / slotRatio;
|
||||||
|
cropY = (img.height - cropHeight) / 2;
|
||||||
}
|
}
|
||||||
}, [isSelected]);
|
|
||||||
|
setCrop({ x: cropX, y: cropY, width: cropWidth, height: cropHeight });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [slot, src]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -40,6 +64,7 @@ export const ImageElement = memo(function ImageElement({
|
|||||||
height={height}
|
height={height}
|
||||||
rotation={rotation}
|
rotation={rotation}
|
||||||
src={src}
|
src={src}
|
||||||
|
crop={crop}
|
||||||
draggable
|
draggable
|
||||||
onClick={onSelect}
|
onClick={onSelect}
|
||||||
onTap={onSelect}
|
onTap={onSelect}
|
||||||
@@ -77,6 +102,10 @@ export const ImageElement = memo(function ImageElement({
|
|||||||
<Transformer
|
<Transformer
|
||||||
ref={trRef}
|
ref={trRef}
|
||||||
boundBoxFunc={(oldBox, newBox) => {
|
boundBoxFunc={(oldBox, newBox) => {
|
||||||
|
// Respect slot boundaries during resize
|
||||||
|
if (slot && dragBoundFunc) {
|
||||||
|
return dragBoundFunc(oldBox, newBox);
|
||||||
|
}
|
||||||
// Limit resize to minimum size
|
// Limit resize to minimum size
|
||||||
if (newBox.width < 20 || newBox.height < 20) {
|
if (newBox.width < 20 || newBox.height < 20) {
|
||||||
return oldBox;
|
return oldBox;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const TABS = [
|
|||||||
{ id: 'templates', label: 'Templates', icon: '📋' },
|
{ id: 'templates', label: 'Templates', icon: '📋' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function Sidebar({ onAddImage, onAddSticker, onAddText, onAddTemplate, onSlotImageUpload }) {
|
export function Sidebar({ onAddImage, onAddSticker, onAddText, onAddTemplate, onSlotImageUpload, elements }) {
|
||||||
const [activeTab, setActiveTab] = useState('upload');
|
const [activeTab, setActiveTab] = useState('upload');
|
||||||
|
|
||||||
const renderTabContent = () => {
|
const renderTabContent = () => {
|
||||||
@@ -23,7 +23,7 @@ export function Sidebar({ onAddImage, onAddSticker, onAddText, onAddTemplate, on
|
|||||||
case 'text':
|
case 'text':
|
||||||
return <TextTab onAddText={onAddText} />;
|
return <TextTab onAddText={onAddText} />;
|
||||||
case 'templates':
|
case 'templates':
|
||||||
return <TemplatesTab onAddTemplate={onAddTemplate} onSlotImageUpload={onSlotImageUpload} />;
|
return <TemplatesTab onAddTemplate={onAddTemplate} onSlotImageUpload={onSlotImageUpload} elements={elements} />;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,9 +16,13 @@ function getCategoryEmoji(category) {
|
|||||||
return emojis[category] || '🎨';
|
return emojis[category] || '🎨';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TemplatesTab({ onAddTemplate, onSlotImageUpload }) {
|
export function TemplatesTab({ onAddTemplate, onSlotImageUpload, elements = [] }) {
|
||||||
const [selectedTemplateId, setSelectedTemplateId] = useState(null);
|
const [selectedTemplateId, setSelectedTemplateId] = useState(null);
|
||||||
const [uploadSlotId, setUploadSlotId] = useState(null);
|
const [uploadSlotId, setUploadSlotId] = useState(null);
|
||||||
|
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
||||||
|
const [pendingTemplate, setPendingTemplate] = useState(null);
|
||||||
|
|
||||||
|
const hasElements = elements && elements.length > 0;
|
||||||
|
|
||||||
const templates = [
|
const templates = [
|
||||||
{
|
{
|
||||||
@@ -36,9 +40,29 @@ export function TemplatesTab({ onAddTemplate, onSlotImageUpload }) {
|
|||||||
})),
|
})),
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleSelectTemplate = (template) => {
|
const performTemplateSwitch = (template) => {
|
||||||
setSelectedTemplateId(template.id);
|
setSelectedTemplateId(template.id);
|
||||||
onAddTemplate(template.id);
|
onAddTemplate(template.id);
|
||||||
|
setPendingTemplate(null);
|
||||||
|
setShowConfirmDialog(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectTemplate = (template) => {
|
||||||
|
if (hasElements) {
|
||||||
|
setPendingTemplate(template);
|
||||||
|
setShowConfirmDialog(true);
|
||||||
|
} else {
|
||||||
|
performTemplateSwitch(template);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmSwitch = () => {
|
||||||
|
performTemplateSwitch(pendingTemplate);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelSwitch = () => {
|
||||||
|
setPendingTemplate(null);
|
||||||
|
setShowConfirmDialog(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSlotClick = (slotId) => {
|
const handleSlotClick = (slotId) => {
|
||||||
@@ -63,24 +87,6 @@ export function TemplatesTab({ onAddTemplate, onSlotImageUpload }) {
|
|||||||
// Get slots for selected template
|
// Get slots for selected template
|
||||||
const selectedTemplate = TEMPLATES.find(t => t.id === selectedTemplateId);
|
const selectedTemplate = TEMPLATES.find(t => t.id === selectedTemplateId);
|
||||||
const slots = selectedTemplate?.slots || [];
|
const slots = selectedTemplate?.slots || [];
|
||||||
const templates = [
|
|
||||||
{
|
|
||||||
id: 'freeform',
|
|
||||||
name: 'Freeform',
|
|
||||||
description: 'No template - design freely',
|
|
||||||
thumbnail: '🎨',
|
|
||||||
},
|
|
||||||
...TEMPLATES.map(t => ({
|
|
||||||
id: t.id,
|
|
||||||
name: t.name,
|
|
||||||
description: t.description,
|
|
||||||
thumbnail: getCategoryEmoji(t.category),
|
|
||||||
})),
|
|
||||||
];
|
|
||||||
|
|
||||||
const handleSelectTemplate = (template) => {
|
|
||||||
onAddTemplate(template.id);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Hidden file input for slot image uploads
|
// Hidden file input for slot image uploads
|
||||||
const renderFileInput = () => (
|
const renderFileInput = () => (
|
||||||
@@ -252,6 +258,80 @@ export function TemplatesTab({ onAddTemplate, onSlotImageUpload }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{renderSlotUploads()}
|
{renderSlotUploads()}
|
||||||
|
|
||||||
|
{/* Confirmation dialog for template switch */}
|
||||||
|
{showConfirmDialog && (
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
background: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
zIndex: 100,
|
||||||
|
borderRadius: 'var(--radius-md)',
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
background: 'var(--bg-primary)',
|
||||||
|
borderRadius: 'var(--radius-md)',
|
||||||
|
padding: '1.25rem',
|
||||||
|
maxWidth: '320px',
|
||||||
|
boxShadow: 'var(--shadow-lg)',
|
||||||
|
}}>
|
||||||
|
<h4 style={{
|
||||||
|
margin: '0 0 0.75rem 0',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: '600',
|
||||||
|
color: 'var(--text-primary)',
|
||||||
|
}}>
|
||||||
|
Switch Template?
|
||||||
|
</h4>
|
||||||
|
<p style={{
|
||||||
|
margin: '0 0 1.25rem 0',
|
||||||
|
fontSize: '12px',
|
||||||
|
color: 'var(--text-secondary)',
|
||||||
|
lineHeight: '1.4',
|
||||||
|
}}>
|
||||||
|
Your current design has elements on the canvas. Switching templates may discard your work.
|
||||||
|
</p>
|
||||||
|
<div style={{ display: 'flex', gap: '0.5rem', justifyContent: 'flex-end' }}>
|
||||||
|
<button
|
||||||
|
onClick={handleCancelSwitch}
|
||||||
|
style={{
|
||||||
|
padding: '0.5rem 0.75rem',
|
||||||
|
border: `1px solid var(--border)`,
|
||||||
|
borderRadius: 'var(--radius-sm)',
|
||||||
|
background: 'var(--bg-primary)',
|
||||||
|
color: 'var(--text-primary)',
|
||||||
|
fontSize: '12px',
|
||||||
|
fontWeight: 500,
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleConfirmSwitch}
|
||||||
|
style={{
|
||||||
|
padding: '0.5rem 0.75rem',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: 'var(--radius-sm)',
|
||||||
|
background: 'var(--accent)',
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: '12px',
|
||||||
|
fontWeight: 600,
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Switch Anyway
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,18 @@ export const TEMPLATES = [
|
|||||||
aspectRatio: 1,
|
aspectRatio: 1,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
layers: {
|
||||||
|
background: {
|
||||||
|
type: 'rect',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 300,
|
||||||
|
height: 300,
|
||||||
|
fill: '#1e40af',
|
||||||
|
opacity: 0.08,
|
||||||
|
},
|
||||||
|
overlay: null,
|
||||||
|
},
|
||||||
elements: [
|
elements: [
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
@@ -47,6 +59,18 @@ export const TEMPLATES = [
|
|||||||
name: 'Band Merch',
|
name: 'Band Merch',
|
||||||
category: 'Music',
|
category: 'Music',
|
||||||
description: 'Classic band t-shirt design',
|
description: 'Classic band t-shirt design',
|
||||||
|
layers: {
|
||||||
|
background: {
|
||||||
|
type: 'rect',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 300,
|
||||||
|
height: 300,
|
||||||
|
fill: '#000000',
|
||||||
|
opacity: 0.12,
|
||||||
|
},
|
||||||
|
overlay: null,
|
||||||
|
},
|
||||||
elements: [
|
elements: [
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
@@ -85,6 +109,18 @@ export const TEMPLATES = [
|
|||||||
name: 'Minimal Quote',
|
name: 'Minimal Quote',
|
||||||
category: 'Quotes',
|
category: 'Quotes',
|
||||||
description: 'Simple centered quote design',
|
description: 'Simple centered quote design',
|
||||||
|
layers: {
|
||||||
|
background: {
|
||||||
|
type: 'rect',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 300,
|
||||||
|
height: 300,
|
||||||
|
fill: '#f1f5f9',
|
||||||
|
opacity: 0.05,
|
||||||
|
},
|
||||||
|
overlay: null,
|
||||||
|
},
|
||||||
elements: [
|
elements: [
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
@@ -113,6 +149,18 @@ export const TEMPLATES = [
|
|||||||
name: 'Funny Cat',
|
name: 'Funny Cat',
|
||||||
category: 'Animals',
|
category: 'Animals',
|
||||||
description: 'Cute cat with funny text',
|
description: 'Cute cat with funny text',
|
||||||
|
layers: {
|
||||||
|
background: {
|
||||||
|
type: 'rect',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 300,
|
||||||
|
height: 300,
|
||||||
|
fill: '#fef3c7',
|
||||||
|
opacity: 0.08,
|
||||||
|
},
|
||||||
|
overlay: null,
|
||||||
|
},
|
||||||
elements: [
|
elements: [
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
@@ -141,6 +189,18 @@ export const TEMPLATES = [
|
|||||||
name: 'Gradient Vibes',
|
name: 'Gradient Vibes',
|
||||||
category: 'Abstract',
|
category: 'Abstract',
|
||||||
description: 'Modern gradient text design',
|
description: 'Modern gradient text design',
|
||||||
|
layers: {
|
||||||
|
background: {
|
||||||
|
type: 'rect',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 300,
|
||||||
|
height: 300,
|
||||||
|
fill: '#f3e8ff',
|
||||||
|
opacity: 0.1,
|
||||||
|
},
|
||||||
|
overlay: null,
|
||||||
|
},
|
||||||
elements: [
|
elements: [
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
@@ -189,6 +249,18 @@ export const TEMPLATES = [
|
|||||||
name: 'Vintage Badge',
|
name: 'Vintage Badge',
|
||||||
category: 'Vintage',
|
category: 'Vintage',
|
||||||
description: 'Retro badge style design',
|
description: 'Retro badge style design',
|
||||||
|
layers: {
|
||||||
|
background: {
|
||||||
|
type: 'rect',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 300,
|
||||||
|
height: 300,
|
||||||
|
fill: '#78716c',
|
||||||
|
opacity: 0.08,
|
||||||
|
},
|
||||||
|
overlay: null,
|
||||||
|
},
|
||||||
elements: [
|
elements: [
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
@@ -237,6 +309,18 @@ export const TEMPLATES = [
|
|||||||
name: 'Nature Lover',
|
name: 'Nature Lover',
|
||||||
category: 'Nature',
|
category: 'Nature',
|
||||||
description: 'Mountain and nature themed',
|
description: 'Mountain and nature themed',
|
||||||
|
layers: {
|
||||||
|
background: {
|
||||||
|
type: 'rect',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 300,
|
||||||
|
height: 300,
|
||||||
|
fill: '#059669',
|
||||||
|
opacity: 0.08,
|
||||||
|
},
|
||||||
|
overlay: null,
|
||||||
|
},
|
||||||
elements: [
|
elements: [
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
@@ -275,6 +359,18 @@ export const TEMPLATES = [
|
|||||||
name: 'Tech Geek',
|
name: 'Tech Geek',
|
||||||
category: 'Tech',
|
category: 'Tech',
|
||||||
description: 'Programming themed design',
|
description: 'Programming themed design',
|
||||||
|
layers: {
|
||||||
|
background: {
|
||||||
|
type: 'rect',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 300,
|
||||||
|
height: 300,
|
||||||
|
fill: '#3b82f6',
|
||||||
|
opacity: 0.08,
|
||||||
|
},
|
||||||
|
overlay: null,
|
||||||
|
},
|
||||||
elements: [
|
elements: [
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
@@ -309,6 +405,7 @@ export const TEMPLATES = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
];
|
||||||
|
|
||||||
export const TEMPLATE_CATEGORIES = [
|
export const TEMPLATE_CATEGORIES = [
|
||||||
'All',
|
'All',
|
||||||
|
|||||||
Reference in New Issue
Block a user