From 3d412600d56c89bfe3eb4f44081e867dbaad8124 Mon Sep 17 00:00:00 2001 From: Khalid A Date: Wed, 22 Apr 2026 05:21:10 -0500 Subject: [PATCH] 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 --- client/src/App.jsx | 125 ++++++++++++++++-- client/src/components/canvas/DesignCanvas.jsx | 3 +- client/src/components/canvas/ImageElement.jsx | 41 +++++- client/src/components/sidebar/Sidebar.jsx | 4 +- .../src/components/sidebar/TemplatesTab.jsx | 120 ++++++++++++++--- client/src/constants/templates.js | 97 ++++++++++++++ 6 files changed, 353 insertions(+), 37 deletions(-) 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',