diff --git a/client/src/App.jsx b/client/src/App.jsx
index 7ce0d97..2bb70e2 100644
--- a/client/src/App.jsx
+++ b/client/src/App.jsx
@@ -13,6 +13,9 @@ import { TEMPLATES } from './constants/templates';
function App() {
const [editingElement, setEditingElement] = useState(null);
+ const [pendingImage, setPendingImage] = useState(null);
+ const [showBgRemovalPrompt, setShowBgRemovalPrompt] = useState(false);
+ const [editingElementId, setEditingElementId] = useState(null);
const {
elements,
@@ -128,18 +131,50 @@ function App() {
// Handle photo editing
const handleEditPhoto = (element) => {
- setEditingElement(element);
+ setEditingElementId(element.id);
+ setPendingImage({ src: element.src, id: element.id });
+ setShowBgRemovalPrompt(false);
};
const handlePhotoEditComplete = (editedImageUrl) => {
- if (editingElement) {
- updateElement(editingElement.id, { src: editedImageUrl });
- }
- setEditingElement(null);
+ setPendingImage(prev => ({ ...prev, editedUrl: editedImageUrl }));
+ setShowBgRemovalPrompt(true);
};
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 (
@@ -157,6 +192,7 @@ function App() {
onAddText={handleAddText}
onAddTemplate={handleAddTemplate}
onSlotImageUpload={handleSlotImageUpload}
+ elements={elements}
/>
{/* Center Canvas Area */}
@@ -280,13 +316,86 @@ function App() {
/>
{/* Photo Pre-Editor Modal */}
- {editingElement && (
+ {pendingImage && (
)}
+
+ {/* Background removal prompt after Filerobot edit */}
+ {showBgRemovalPrompt && (
+
+
+
+ Remove Background?
+
+
+ AI can automatically remove the background from your image.
+
+
+
+
+
+
+
+ )}
);
}
diff --git a/client/src/components/canvas/DesignCanvas.jsx b/client/src/components/canvas/DesignCanvas.jsx
index 0f5ddc1..cf14d77 100644
--- a/client/src/components/canvas/DesignCanvas.jsx
+++ b/client/src/components/canvas/DesignCanvas.jsx
@@ -61,6 +61,7 @@ export const DesignCanvas = memo(function DesignCanvas({
{elements.map((el) => {
if (el.type === 'image') {
+ const slot = el.slotId ? slots.find(s => s.id === el.slotId) : null;
return (
onSelect(el.id)}
onUpdate={(attrs) => onUpdate(el.id, attrs)}
diff --git a/client/src/components/canvas/ImageElement.jsx b/client/src/components/canvas/ImageElement.jsx
index 5971bb3..0acdb41 100644
--- a/client/src/components/canvas/ImageElement.jsx
+++ b/client/src/components/canvas/ImageElement.jsx
@@ -2,9 +2,9 @@ import { useEffect, useState, memo } from 'react';
import { Image, Transformer } from 'react-konva';
import useImage from 'use-image';
-function URLImage({ src, ...props }) {
+function URLImage({ src, crop, ...props }) {
const [img] = useImage(src, 'anonymous');
- return ;
+ return ;
}
export const ImageElement = memo(function ImageElement({
@@ -15,20 +15,44 @@ export const ImageElement = memo(function ImageElement({
height,
rotation,
src,
+ slot,
isSelected,
onSelect,
onUpdate,
onCommit,
+ dragBoundFunc,
}) {
const shapeRef = null;
const trRef = null;
+ const [crop, setCrop] = useState(null);
+ // Calculate crop for slotted images using object-fit: cover logic
useEffect(() => {
- if (isSelected && trRef.current) {
- trRef.current.nodes([shapeRef.current]);
- trRef.current.getLayer().batchDraw();
+ if (slot && src) {
+ const img = new Image();
+ 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;
+ }
+
+ setCrop({ x: cropX, y: cropY, width: cropWidth, height: cropHeight });
+ };
}
- }, [isSelected]);
+ }, [slot, src]);
return (
<>
@@ -40,6 +64,7 @@ export const ImageElement = memo(function ImageElement({
height={height}
rotation={rotation}
src={src}
+ crop={crop}
draggable
onClick={onSelect}
onTap={onSelect}
@@ -77,6 +102,10 @@ export const ImageElement = memo(function ImageElement({
{
+ // Respect slot boundaries during resize
+ if (slot && dragBoundFunc) {
+ return dragBoundFunc(oldBox, newBox);
+ }
// Limit resize to minimum size
if (newBox.width < 20 || newBox.height < 20) {
return oldBox;
diff --git a/client/src/components/sidebar/Sidebar.jsx b/client/src/components/sidebar/Sidebar.jsx
index 99e5215..456b3e5 100644
--- a/client/src/components/sidebar/Sidebar.jsx
+++ b/client/src/components/sidebar/Sidebar.jsx
@@ -11,7 +11,7 @@ const TABS = [
{ 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 renderTabContent = () => {
@@ -23,7 +23,7 @@ export function Sidebar({ onAddImage, onAddSticker, onAddText, onAddTemplate, on
case 'text':
return ;
case 'templates':
- return ;
+ return ;
default:
return null;
}
diff --git a/client/src/components/sidebar/TemplatesTab.jsx b/client/src/components/sidebar/TemplatesTab.jsx
index 2ecf1eb..a5beffb 100644
--- a/client/src/components/sidebar/TemplatesTab.jsx
+++ b/client/src/components/sidebar/TemplatesTab.jsx
@@ -16,9 +16,13 @@ function getCategoryEmoji(category) {
return emojis[category] || '🎨';
}
-export function TemplatesTab({ onAddTemplate, onSlotImageUpload }) {
+export function TemplatesTab({ onAddTemplate, onSlotImageUpload, elements = [] }) {
const [selectedTemplateId, setSelectedTemplateId] = 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 = [
{
@@ -36,9 +40,29 @@ export function TemplatesTab({ onAddTemplate, onSlotImageUpload }) {
})),
];
- const handleSelectTemplate = (template) => {
+ const performTemplateSwitch = (template) => {
setSelectedTemplateId(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) => {
@@ -63,24 +87,6 @@ export function TemplatesTab({ onAddTemplate, onSlotImageUpload }) {
// Get slots for selected template
const selectedTemplate = TEMPLATES.find(t => t.id === selectedTemplateId);
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
const renderFileInput = () => (
@@ -252,6 +258,80 @@ export function TemplatesTab({ onAddTemplate, onSlotImageUpload }) {
{renderSlotUploads()}
+
+ {/* Confirmation dialog for template switch */}
+ {showConfirmDialog && (
+
+
+
+ Switch Template?
+
+
+ Your current design has elements on the canvas. Switching templates may discard your work.
+
+
+
+
+
+
+
+ )}
);
}
diff --git a/client/src/constants/templates.js b/client/src/constants/templates.js
index 9b08438..074392c 100644
--- a/client/src/constants/templates.js
+++ b/client/src/constants/templates.js
@@ -19,6 +19,18 @@ export const TEMPLATES = [
aspectRatio: 1,
},
],
+ layers: {
+ background: {
+ type: 'rect',
+ x: 0,
+ y: 0,
+ width: 300,
+ height: 300,
+ fill: '#1e40af',
+ opacity: 0.08,
+ },
+ overlay: null,
+ },
elements: [
{
type: 'text',
@@ -47,6 +59,18 @@ export const TEMPLATES = [
name: 'Band Merch',
category: 'Music',
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: [
{
type: 'text',
@@ -85,6 +109,18 @@ export const TEMPLATES = [
name: 'Minimal Quote',
category: 'Quotes',
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: [
{
type: 'text',
@@ -113,6 +149,18 @@ export const TEMPLATES = [
name: 'Funny Cat',
category: 'Animals',
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: [
{
type: 'text',
@@ -141,6 +189,18 @@ export const TEMPLATES = [
name: 'Gradient Vibes',
category: 'Abstract',
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: [
{
type: 'text',
@@ -189,6 +249,18 @@ export const TEMPLATES = [
name: 'Vintage Badge',
category: 'Vintage',
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: [
{
type: 'text',
@@ -237,6 +309,18 @@ export const TEMPLATES = [
name: 'Nature Lover',
category: 'Nature',
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: [
{
type: 'text',
@@ -275,6 +359,18 @@ export const TEMPLATES = [
name: 'Tech Geek',
category: 'Tech',
description: 'Programming themed design',
+ layers: {
+ background: {
+ type: 'rect',
+ x: 0,
+ y: 0,
+ width: 300,
+ height: 300,
+ fill: '#3b82f6',
+ opacity: 0.08,
+ },
+ overlay: null,
+ },
elements: [
{
type: 'text',
@@ -309,6 +405,7 @@ export const TEMPLATES = [
],
},
];
+];
export const TEMPLATE_CATEGORIES = [
'All',