diff --git a/src/components/DesignCanvasWrapper.tsx b/src/components/DesignCanvasWrapper.tsx new file mode 100644 index 0000000..a4181e5 --- /dev/null +++ b/src/components/DesignCanvasWrapper.tsx @@ -0,0 +1,13 @@ +'use client'; + +import FabricDesignCanvasV2 from './FabricDesignCanvasV2'; +import type { ProductDTO } from '@/lib/types'; + +interface DesignCanvasWrapperProps { + product: ProductDTO; + customPhoto?: string; +} + +export default function DesignCanvasWrapper({ product, customPhoto }: DesignCanvasWrapperProps) { + return ; +} diff --git a/src/components/FabricDesignCanvasV2.tsx b/src/components/FabricDesignCanvasV2.tsx new file mode 100644 index 0000000..a8b717d --- /dev/null +++ b/src/components/FabricDesignCanvasV2.tsx @@ -0,0 +1,390 @@ +'use client'; + +import { useEffect, useRef, useState } from 'react'; +import { v4 as uuid } from 'uuid'; +import type { DesignElement, ProductDTO } from '@/lib/types'; + +const DISPLAY = 560; // Canvas display size +const FONTS = ['Arial', 'Georgia', 'Courier New', 'Times New Roman', 'Verdana', 'Comic Sans MS']; + +interface FabricDesignCanvasV2Props { + product: ProductDTO; + customPhoto?: string; +} + +// Lazy load Fabric.js +let fabricLib: any = null; + +const loadFabric = async () => { + if (fabricLib) return fabricLib; + + try { + console.log('Loading fabric.js...'); + // Import fabric.js - it exports Canvas, Text, Image, Rect as named exports + const mod = await import('fabric'); + console.log('Fabric module loaded, checking exports...'); + console.log('Available exports:', Object.keys(mod).slice(0, 20)); + + if (!mod.Canvas) { + throw new Error('Canvas not found in fabric module exports'); + } + + fabricLib = { + Canvas: mod.Canvas, + Text: mod.Text, + Image: mod.Image, + Rect: mod.Rect, + }; + console.log('Fabric.js setup completed'); + return fabricLib; + } catch (err) { + console.error('Failed to import fabric.js:', err); + // Return empty object instead of throwing to prevent infinite errors + return {}; + } +}; + +export default function FabricDesignCanvasV2({ product, customPhoto }: FabricDesignCanvasV2Props) { + const canvasRef = useRef(null); + const fabricCanvasRef = useRef(null); + const isMountedRef = useRef(false); + const [design, setDesign] = useState<{ front: DesignElement[]; back: DesignElement[] }>({ + front: [], + back: [], + }); + const [side, setSide] = useState<'front' | 'back'>('front'); + const [selectedColor, setSelectedColor] = useState(product.colors[0]); + const [history, setHistory] = useState>([]); + const [historyIndex, setHistoryIndex] = useState(-1); + const [isLoaded, setIsLoaded] = useState(false); + + // Track client mounting to avoid hydration issues + useEffect(() => { + isMountedRef.current = true; + }, []); + + // Initialize Fabric.js canvas only on client + useEffect(() => { + if (!isMountedRef.current) return; + + let mounted = true; + const init = async () => { + if (!canvasRef.current || fabricCanvasRef.current) return; + + try { + const fabLib = await loadFabric(); + if (!mounted) return; + + const Canvas = fabLib.Canvas; + if (!Canvas) { + throw new Error(`Canvas is undefined. fabLib keys: ${Object.keys(fabLib).join(', ')}`); + } + + // Create canvas instance + const fabricCanvas = new Canvas(canvasRef.current, { + width: DISPLAY, + height: DISPLAY, + backgroundColor: 'transparent', + }); + + fabricCanvasRef.current = fabricCanvas; + + // Handle object selection + fabricCanvas.on('object:added', () => saveHistory()); + fabricCanvas.on('object:modified', () => saveHistory()); + fabricCanvas.on('object:removed', () => saveHistory()); + + if (mounted) { + setIsLoaded(true); + } + } catch (err) { + console.error('Failed to initialize Fabric.js canvas:', err); + if (mounted) { + setIsLoaded(true); // Still mark as loaded to prevent infinite retry + } + } + }; + + init(); + + return () => { + mounted = false; + if (fabricCanvasRef.current) { + try { + fabricCanvasRef.current.dispose(); + fabricCanvasRef.current = null; + } catch (e) { + // Ignore disposal errors + } + } + }; + }, []); + + // Render design elements on canvas + useEffect(() => { + if (!fabricCanvasRef.current || !isLoaded) return; + + const canvas = fabricCanvasRef.current; + canvas.clear(); + + const render = async () => { + try { + const { Text, Image, Rect } = await loadFabric(); + + const printArea = side === 'front' ? product.printArea : product.printAreaBack || product.printArea; + + // Print area is stored as 0-1 decimals; convert to pixels on 560x560 canvas + const minX = Math.round(printArea.xPct * DISPLAY); + const minY = Math.round(printArea.yPct * DISPLAY); + const width = Math.round(printArea.wPct * DISPLAY); + const height = Math.round(printArea.hPct * DISPLAY); + + const rect = new Rect({ + left: minX, + top: minY, + width, + height, + fill: 'transparent', + stroke: '#E5227E', + strokeDashArray: [5, 5], + strokeWidth: 2, + selectable: false, + hasControls: false, + }); + canvas.add(rect); + + // Render design elements + const elements = side === 'front' ? design.front : design.back; + elements.forEach((el) => { + if (el.type === 'text') { + const text = new Text(el.text, { + left: el.x, + top: el.y, + fontSize: el.fontSize, + fontFamily: el.fontFamily, + fill: el.fill, + selectable: true, + hasControls: true, + }); + canvas.add(text); + } + // TODO: Add image support after fixing text rendering + }); + + canvas.renderAll(); + } catch (err) { + console.error('Failed to render canvas:', err); + } + }; + + render(); + }, [design, side, isLoaded]); + + const renderProductImage = async () => { + console.log('renderProductImage called'); + if (!fabricCanvasRef.current) { + console.log('Canvas ref not ready'); + return; + } + const canvas = fabricCanvasRef.current; + const printArea = side === 'front' ? product.printArea : product.printAreaBack || product.printArea; + + // Get Image and Rect classes + const { Image, Rect } = await loadFabric(); + + // Determine which image to use + let imageUrl: string | null = null; + + console.log('Rendering product image for side:', side); + console.log('Product data:', { colorPhotos: product.colorPhotos, imageUrl: product.imageUrl, colorPhotosBack: product.colorPhotosBack, imageUrlBack: product.imageUrlBack, selectedColor }); + + if (customPhoto && side === 'front') { + imageUrl = customPhoto; + console.log('Using customPhoto:', imageUrl); + } else if (side === 'front' && product.colorPhotos?.[selectedColor]) { + imageUrl = product.colorPhotos[selectedColor]; + console.log('Using colorPhoto for color:', selectedColor, imageUrl); + } else if (side === 'front' && product.imageUrl) { + imageUrl = product.imageUrl; + console.log('Using imageUrl:', imageUrl); + } else if (side === 'back' && product.colorPhotosBack?.[selectedColor]) { + imageUrl = product.colorPhotosBack[selectedColor]; + console.log('Using colorPhotoBack for color:', selectedColor, imageUrl); + } else if (side === 'back' && product.imageUrlBack) { + imageUrl = product.imageUrlBack; + console.log('Using imageUrlBack:', imageUrl); + } else if (side === 'back' && product.colorPhotos?.[selectedColor]) { + imageUrl = product.colorPhotos[selectedColor]; + console.log('Fallback: Using colorPhoto for back:', selectedColor, imageUrl); + } + + // Draw print area guide only + const minX = Math.round(printArea.xPct * DISPLAY); + const minY = Math.round(printArea.yPct * DISPLAY); + const width = Math.round(printArea.wPct * DISPLAY); + const height = Math.round(printArea.hPct * DISPLAY); + + const rect = new Rect({ + left: minX, + top: minY, + width, + height, + fill: 'transparent', + stroke: '#E5227E', + strokeDashArray: [5, 5], + strokeWidth: 2, + selectable: false, + hasControls: false, + }); + canvas.add(rect); + }; + + const saveHistory = () => { + setHistory((prev) => [...prev.slice(0, historyIndex + 1), { ...design }]); + setHistoryIndex((prev) => prev + 1); + }; + + const addText = () => { + const newElement: DesignElement = { + id: uuid(), + type: 'text', + text: 'Click to edit', + x: DISPLAY / 2 - 50, + y: DISPLAY / 2 - 12, + fontSize: 24, + fontFamily: 'Arial', + fill: '#000000', + rotation: 0, + }; + + setDesign((prev) => ({ + ...prev, + [side]: [...prev[side], newElement], + })); + }; + + const addImage = () => { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = 'image/*'; + input.onchange = (e) => { + const file = (e.target as HTMLInputElement).files?.[0]; + if (file) { + const reader = new FileReader(); + reader.onload = (evt) => { + const newElement: DesignElement = { + id: uuid(), + type: 'image', + src: evt.target?.result as string, + x: DISPLAY / 2 - 50, + y: DISPLAY / 2 - 50, + width: 100, + height: 100, + rotation: 0, + }; + setDesign((prev) => ({ + ...prev, + [side]: [...prev[side], newElement], + })); + }; + reader.readAsDataURL(file); + } + }; + input.click(); + }; + + return ( +
+
+ {/* Canvas Area */} +
+
+ + {product.printAreaBack && ( + + )} +
+ + {/* Color Swatches */} +
+

Garment Color:

+
+ {product.colors.map((color) => ( +
+
+ + {/* Canvas with Product Image */} +
+ {/* Product image background */} + Product + {/* Fabric canvas overlay */} + +
+ + {/* Controls */} +
+ + +
+
+ + {/* Sidebar */} +
+
+

Properties

+

Click on text or image to edit

+
+

+ £{(product.effectivePrice / 100).toFixed(2)} +

+ +
+
+
+
+
+ ); +}