@merchi/product-editor 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/.turbo/turbo-build.log +1 -0
  2. package/README.md +174 -0
  3. package/dist/components/AlignMenu.d.ts +3 -0
  4. package/dist/components/AlignMenu.js +61 -0
  5. package/dist/components/FloatingToolbar.d.ts +4 -0
  6. package/dist/components/FloatingToolbar.js +97 -0
  7. package/dist/components/FormatButtons.d.ts +2 -0
  8. package/dist/components/FormatButtons.js +22 -0
  9. package/dist/components/ImageNavButton.d.ts +7 -0
  10. package/dist/components/ImageNavButton.js +14 -0
  11. package/dist/components/ImageZoomModal.d.ts +12 -0
  12. package/dist/components/ImageZoomModal.js +129 -0
  13. package/dist/components/LayerPanel.d.ts +4 -0
  14. package/dist/components/LayerPanel.js +223 -0
  15. package/dist/components/LoadingOverlay.d.ts +9 -0
  16. package/dist/components/LoadingOverlay.js +17 -0
  17. package/dist/components/ProductEditor.d.ts +4 -0
  18. package/dist/components/ProductEditor.js +67 -0
  19. package/dist/components/ProductImageGallery.d.ts +6 -0
  20. package/dist/components/ProductImageGallery.js +148 -0
  21. package/dist/components/ProductPreviews.d.ts +7 -0
  22. package/dist/components/ProductPreviews.js +281 -0
  23. package/dist/components/TextToolbar.d.ts +4 -0
  24. package/dist/components/TextToolbar.js +117 -0
  25. package/dist/components/Toolbar.d.ts +3 -0
  26. package/dist/components/Toolbar.js +47 -0
  27. package/dist/config/colorConfig.d.ts +1 -0
  28. package/dist/config/colorConfig.js +10 -0
  29. package/dist/config/fontConfig.d.ts +5 -0
  30. package/dist/config/fontConfig.js +21 -0
  31. package/dist/context/ProductEditorContext.d.ts +60 -0
  32. package/dist/context/ProductEditorContext.js +743 -0
  33. package/dist/hooks/useSwipeNavigate.d.ts +11 -0
  34. package/dist/hooks/useSwipeNavigate.js +35 -0
  35. package/dist/index.d.ts +6 -0
  36. package/dist/index.js +30 -0
  37. package/dist/styles/BottomPreviewDisplay.css +65 -0
  38. package/dist/styles/FloatingToolbar.css +41 -0
  39. package/dist/styles/ImageGallery.css +147 -0
  40. package/dist/styles/ImageNavButton.css +35 -0
  41. package/dist/styles/ImageZoomModal.css +101 -0
  42. package/dist/styles/LayerPanel.css +179 -0
  43. package/dist/styles/LoadingOverlay.css +48 -0
  44. package/dist/styles/ProductEditor.css +192 -0
  45. package/dist/styles/TextToolbar.css +327 -0
  46. package/dist/styles/Toolbar.css +125 -0
  47. package/dist/styles/index.css +5 -0
  48. package/dist/styles/textToolbarSelectStyles.d.ts +3 -0
  49. package/dist/styles/textToolbarSelectStyles.js +30 -0
  50. package/dist/types.d.ts +228 -0
  51. package/dist/types.js +17 -0
  52. package/dist/utils/canvasAddText.d.ts +11 -0
  53. package/dist/utils/canvasAddText.js +66 -0
  54. package/dist/utils/canvasUtils.d.ts +37 -0
  55. package/dist/utils/canvasUtils.js +342 -0
  56. package/dist/utils/deviceUtils.d.ts +5 -0
  57. package/dist/utils/deviceUtils.js +33 -0
  58. package/dist/utils/draftTemplateUtils.d.ts +23 -0
  59. package/dist/utils/draftTemplateUtils.js +66 -0
  60. package/dist/utils/grid.d.ts +32 -0
  61. package/dist/utils/grid.js +151 -0
  62. package/dist/utils/imageUtils.d.ts +12 -0
  63. package/dist/utils/imageUtils.js +178 -0
  64. package/dist/utils/job.d.ts +40 -0
  65. package/dist/utils/job.js +115 -0
  66. package/dist/utils/keyboard.d.ts +1 -0
  67. package/dist/utils/keyboard.js +26 -0
  68. package/dist/utils/previewUtils.d.ts +9 -0
  69. package/dist/utils/previewUtils.js +39 -0
  70. package/dist/utils/psdConverter.d.ts +40 -0
  71. package/dist/utils/psdConverter.js +516 -0
  72. package/dist/utils/psdRenderUtils.d.ts +12 -0
  73. package/dist/utils/psdRenderUtils.js +465 -0
  74. package/dist/utils/renderUtils.d.ts +2 -0
  75. package/dist/utils/renderUtils.js +85 -0
  76. package/package.json +66 -0
  77. package/src/components/AlignMenu.tsx +49 -0
  78. package/src/components/FloatingToolbar.tsx +83 -0
  79. package/src/components/FormatButtons.tsx +31 -0
  80. package/src/components/ImageNavButton.tsx +23 -0
  81. package/src/components/ImageZoomModal.tsx +163 -0
  82. package/src/components/LayerPanel.tsx +256 -0
  83. package/src/components/LoadingOverlay.tsx +27 -0
  84. package/src/components/ProductEditor.tsx +104 -0
  85. package/src/components/ProductImageGallery.tsx +181 -0
  86. package/src/components/ProductPreviews.tsx +243 -0
  87. package/src/components/TextToolbar.tsx +164 -0
  88. package/src/components/Toolbar.tsx +86 -0
  89. package/src/config/colorConfig.ts +7 -0
  90. package/src/config/fontConfig.ts +23 -0
  91. package/src/context/ProductEditorContext.tsx +829 -0
  92. package/src/hooks/useSwipeNavigate.ts +40 -0
  93. package/src/index.ts +6 -0
  94. package/src/styles/BottomPreviewDisplay.css +65 -0
  95. package/src/styles/FloatingToolbar.css +41 -0
  96. package/src/styles/ImageGallery.css +147 -0
  97. package/src/styles/ImageNavButton.css +35 -0
  98. package/src/styles/ImageZoomModal.css +101 -0
  99. package/src/styles/LayerPanel.css +179 -0
  100. package/src/styles/LoadingOverlay.css +48 -0
  101. package/src/styles/ProductEditor.css +192 -0
  102. package/src/styles/TextToolbar.css +327 -0
  103. package/src/styles/Toolbar.css +125 -0
  104. package/src/styles/index.css +5 -0
  105. package/src/styles/textToolbarSelectStyles.ts +60 -0
  106. package/src/types/ag-psd/index.d.ts +1 -0
  107. package/src/types/fabric.d.ts +14 -0
  108. package/src/types/lodash/index.d.ts +1 -0
  109. package/src/types/psd.d.ts +53 -0
  110. package/src/types.ts +247 -0
  111. package/src/utils/canvasAddText.ts +72 -0
  112. package/src/utils/canvasUtils.ts +406 -0
  113. package/src/utils/deviceUtils.ts +31 -0
  114. package/src/utils/draftTemplateUtils.ts +70 -0
  115. package/src/utils/grid.ts +154 -0
  116. package/src/utils/imageUtils.ts +218 -0
  117. package/src/utils/job.ts +128 -0
  118. package/src/utils/keyboard.ts +28 -0
  119. package/src/utils/previewUtils.ts +43 -0
  120. package/src/utils/psdConverter.ts +595 -0
  121. package/src/utils/psdRenderUtils.ts +453 -0
  122. package/src/utils/renderUtils.ts +44 -0
  123. package/tsconfig.json +17 -0
@@ -0,0 +1,83 @@
1
+ import React, { useState, useRef, useEffect } from 'react';
2
+ import Toolbar from './Toolbar';
3
+ import '../styles/FloatingToolbar.css';
4
+ const FloatingToolbar: React.FC = () => {
5
+ const [position, setPosition] = useState({ x: 15, y: 15 });
6
+ const [isDragging, setIsDragging] = useState(false);
7
+ const toolbarRef = useRef<HTMLDivElement>(null);
8
+ const offset = useRef({ x: 0, y: 0 });
9
+
10
+ const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
11
+ if (!toolbarRef.current) return;
12
+ // Prevent dragging if clicking on buttons inside the toolbar
13
+ if ((e.target as HTMLElement).closest('.toolbar-button')) {
14
+ return;
15
+ }
16
+ setIsDragging(true);
17
+ const rect = toolbarRef.current.getBoundingClientRect();
18
+ offset.current = {
19
+ x: e.clientX - rect.left,
20
+ y: e.clientY - rect.top,
21
+ };
22
+ if (toolbarRef.current) {
23
+ toolbarRef.current.style.cursor = 'grabbing';
24
+ }
25
+ e.preventDefault();
26
+ };
27
+
28
+ const handleMouseMove = (e: MouseEvent) => {
29
+ if (!isDragging || !toolbarRef.current) return;
30
+ let newX = e.clientX - offset.current.x;
31
+ let newY = e.clientY - offset.current.y;
32
+
33
+ const parent = toolbarRef.current.parentElement;
34
+ if (parent) {
35
+ const parentRect = parent.getBoundingClientRect();
36
+ const toolbarRect = toolbarRef.current.getBoundingClientRect();
37
+
38
+ newX = Math.max(parentRect.left, Math.min(newX, parentRect.right - toolbarRect.width));
39
+ newY = Math.max(parentRect.top, Math.min(newY, parentRect.bottom - toolbarRect.height));
40
+ newX -= parentRect.left;
41
+ newY -= parentRect.top;
42
+ }
43
+ setPosition({ x: newX, y: newY });
44
+ };
45
+
46
+ const handleMouseUp = () => {
47
+ setIsDragging(false);
48
+ if (toolbarRef.current) {
49
+ toolbarRef.current.style.cursor = 'grab';
50
+ }
51
+ };
52
+
53
+ useEffect(() => {
54
+ if (isDragging) {
55
+ window.addEventListener('mousemove', handleMouseMove);
56
+ window.addEventListener('mouseup', handleMouseUp);
57
+ } else {
58
+ window.removeEventListener('mousemove', handleMouseMove);
59
+ window.removeEventListener('mouseup', handleMouseUp);
60
+ }
61
+
62
+ return () => {
63
+ window.removeEventListener('mousemove', handleMouseMove);
64
+ window.removeEventListener('mouseup', handleMouseUp);
65
+ };
66
+ }, [isDragging]);
67
+
68
+ return (
69
+ <div
70
+ ref={toolbarRef}
71
+ className="floating-toolbar"
72
+ style={{
73
+ left: `${position.x}px`,
74
+ top: `${position.y}px`,
75
+ }}
76
+ onMouseDown={handleMouseDown}
77
+ >
78
+ <Toolbar />
79
+ </div>
80
+ );
81
+ };
82
+
83
+ export default FloatingToolbar;
@@ -0,0 +1,31 @@
1
+ import React from 'react';
2
+ import { useProductEditor } from '../context/ProductEditorContext';
3
+
4
+ export const FormatButtons: React.FC = () => {
5
+ const { selectedTextObject, updateSelectedText } = useProductEditor();
6
+ if (!selectedTextObject || !updateSelectedText) return null;
7
+
8
+ const isBold = selectedTextObject.fontWeight === 'bold';
9
+ const isUnderlined = selectedTextObject.underline === true;
10
+
11
+ return (
12
+ <>
13
+ <button
14
+ type="button"
15
+ className={`toolbar-button bold-button ${isBold ? 'active' : ''}`}
16
+ onClick={() => updateSelectedText({ fontWeight: isBold ? 'normal' : 'bold' })}
17
+ title="Bold"
18
+ >
19
+ <strong>B</strong>
20
+ </button>
21
+ <button
22
+ type="button"
23
+ className={`toolbar-button underline-button ${isUnderlined ? 'active' : ''}`}
24
+ onClick={() => updateSelectedText({ underline: !isUnderlined })}
25
+ title="Underline"
26
+ >
27
+ <strong><u>U</u></strong>
28
+ </button>
29
+ </>
30
+ );
31
+ };
@@ -0,0 +1,23 @@
1
+ import React from 'react';
2
+ import { FormPrevious, FormNext } from "grommet-icons";
3
+
4
+ interface ImageNavButtonProps {
5
+ direction: 'left' | 'right';
6
+ onClick: () => void;
7
+ }
8
+
9
+ const ImageNavButton = ({ direction, onClick }: ImageNavButtonProps) => (
10
+ <button
11
+ onClick={onClick}
12
+ className={`nav-button ${direction}`}
13
+ aria-label={direction === 'left' ? 'Previous image' : 'Next image'}
14
+ type="button"
15
+ >
16
+ {direction === 'left'
17
+ ? <FormPrevious className="nav-button-icon" color="#6b7280" />
18
+ : <FormNext className="nav-button-icon" color="#6b7280" />
19
+ }
20
+ </button>
21
+ );
22
+
23
+ export default ImageNavButton;
@@ -0,0 +1,163 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import { createPortal } from "react-dom";
3
+ import { Close, FormPrevious, FormNext } from "grommet-icons";
4
+ import { motion, AnimatePresence, type Variants } from "framer-motion";
5
+ import { useSwipeNavigate } from "../hooks/useSwipeNavigate";
6
+
7
+ const MODAL_BODY_CLASS = 'has-preview-modal-open';
8
+
9
+ interface ImageZoomModalProps {
10
+ isOpen: boolean;
11
+ onClose: () => void;
12
+ imageUrl: string;
13
+ productName: string;
14
+ totalImages: number;
15
+ currentIndex: number;
16
+ allImages: any[];
17
+ }
18
+
19
+ const ImageZoomModal = ({
20
+ isOpen,
21
+ onClose,
22
+ imageUrl,
23
+ productName,
24
+ totalImages,
25
+ currentIndex,
26
+ allImages
27
+ }: ImageZoomModalProps) => {
28
+ const [modalImageIndex, setModalImageIndex] = useState(currentIndex);
29
+ const [currentImageUrl, setCurrentImageUrl] = useState(imageUrl);
30
+ const [slideDirection, setSlideDirection] = useState<'left' | 'right'>('right');
31
+
32
+ useEffect(() => {
33
+ if (isOpen) {
34
+ document.body.style.overflow = 'hidden';
35
+ document.body.classList.add(MODAL_BODY_CLASS);
36
+ document.documentElement.classList.add(MODAL_BODY_CLASS);
37
+ } else {
38
+ document.body.style.overflow = '';
39
+ document.body.classList.remove(MODAL_BODY_CLASS);
40
+ document.documentElement.classList.remove(MODAL_BODY_CLASS);
41
+ }
42
+
43
+ return () => {
44
+ document.body.style.overflow = '';
45
+ document.body.classList.remove(MODAL_BODY_CLASS);
46
+ document.documentElement.classList.remove(MODAL_BODY_CLASS);
47
+ };
48
+ }, [isOpen]);
49
+
50
+ useEffect(() => {
51
+ setModalImageIndex(currentIndex);
52
+ setCurrentImageUrl(imageUrl);
53
+ setSlideDirection('right');
54
+ }, [currentIndex, imageUrl, isOpen]);
55
+
56
+ const handlePrevious = () => {
57
+ setSlideDirection('left');
58
+ setModalImageIndex((prev) => {
59
+ const newIndex = (prev - 1 + totalImages) % totalImages;
60
+ setCurrentImageUrl(allImages[newIndex].viewUrl);
61
+ return newIndex;
62
+ });
63
+ };
64
+
65
+ const handleNext = () => {
66
+ setSlideDirection('right');
67
+ setModalImageIndex((prev) => {
68
+ const newIndex = (prev + 1) % totalImages;
69
+ setCurrentImageUrl(allImages[newIndex].viewUrl);
70
+ return newIndex;
71
+ });
72
+ };
73
+
74
+ const swipeHandlers = useSwipeNavigate({
75
+ onSwipeLeft: handleNext,
76
+ onSwipeRight: handlePrevious,
77
+ });
78
+
79
+ const slideVariants: Variants = {
80
+ enter: (direction: 'left' | 'right') => ({
81
+ x: direction === 'right' ? 1000 : -1000,
82
+ opacity: 0
83
+ }),
84
+ center: {
85
+ zIndex: 1,
86
+ x: 0,
87
+ opacity: 1,
88
+ transition: {
89
+ x: { type: "spring", stiffness: 300, damping: 30 },
90
+ opacity: { duration: 0.2 }
91
+ }
92
+ },
93
+ exit: (direction: 'left' | 'right') => ({
94
+ zIndex: 0,
95
+ x: direction === 'left' ? 1000 : -1000,
96
+ opacity: 0,
97
+ transition: {
98
+ x: { type: "spring", stiffness: 300, damping: 30 },
99
+ opacity: { duration: 0.05 }
100
+ }
101
+ })
102
+ };
103
+
104
+ if (!isOpen) return null;
105
+
106
+ // Render the modal directly at the document body level
107
+ return createPortal(
108
+ <div className="zoom-modal">
109
+ <div className="zoom-modal-container">
110
+ <button
111
+ onClick={onClose}
112
+ className="close-button"
113
+ type="button"
114
+ >
115
+ <Close width={36} height={36} color="white" />
116
+ </button>
117
+
118
+ {totalImages > 1 && (
119
+ <button
120
+ onClick={handlePrevious}
121
+ className="nav-button-zoom left"
122
+ type="button"
123
+ >
124
+ <FormPrevious width={42} height={42} color="white" />
125
+ </button>
126
+ )}
127
+
128
+ <div
129
+ className="zoom-image-container"
130
+ {...swipeHandlers}
131
+ >
132
+ <AnimatePresence initial={false} custom={slideDirection}>
133
+ <motion.img
134
+ key={modalImageIndex}
135
+ src={currentImageUrl}
136
+ alt={productName}
137
+ className="zoom-image"
138
+ loading="lazy"
139
+ custom={slideDirection}
140
+ variants={slideVariants}
141
+ initial="enter"
142
+ animate="center"
143
+ exit="exit"
144
+ />
145
+ </AnimatePresence>
146
+ </div>
147
+
148
+ {totalImages > 1 && (
149
+ <button
150
+ onClick={handleNext}
151
+ className="nav-button-zoom right"
152
+ type="button"
153
+ >
154
+ <FormNext width={42} height={42} color="white" />
155
+ </button>
156
+ )}
157
+ </div>
158
+ </div>,
159
+ document.body
160
+ );
161
+ };
162
+
163
+ export default ImageZoomModal;
@@ -0,0 +1,256 @@
1
+ import React, { useState, useEffect, useRef } from 'react';
2
+ import { fabric } from 'fabric';
3
+ import { useProductEditor } from '../context/ProductEditorContext';
4
+ import { Close, Drag, TextAlignCenter, Image as ImageIcon, Up, Down } from 'grommet-icons';
5
+ import '../styles/LayerPanel.css';
6
+
7
+ const reorder = (list: any[], startIndex: number, endIndex: number) => {
8
+ const result = Array.from(list);
9
+ const [removed] = result.splice(startIndex, 1);
10
+ result.splice(endIndex, 0, removed);
11
+ return result;
12
+ };
13
+
14
+ const LAYER_ITEM_HEIGHT_PX = 45;
15
+
16
+ const LayerPanel: React.FC = () => {
17
+ const { canvas, toggleLayerPanel, selectedObjectId, selectObject, isMobileView } = useProductEditor();
18
+ const [layers, setLayers] = useState<fabric.Object[]>([]);
19
+ const dragItemIndex = useRef<number | null>(null);
20
+ const dragOverItemIndex = useRef<number | null>(null);
21
+ const [isDraggingOver, setIsDraggingOver] = useState(false);
22
+ const [dragOverIndexVisual, setDragOverIndexVisual] = useState<number | null>(null);
23
+
24
+ useEffect(() => {
25
+ if (canvas) {
26
+ const updateLayers = () => {
27
+ const filteredObjects = canvas.getObjects().filter(obj =>
28
+ (obj.type === 'i-text' || obj.type === 'image') && obj.selectable
29
+ );
30
+ setLayers([...filteredObjects].reverse());
31
+ };
32
+ updateLayers();
33
+
34
+ const handleCanvasChange = () => updateLayers();
35
+ canvas.on('object:added', handleCanvasChange);
36
+ canvas.on('object:removed', handleCanvasChange);
37
+ canvas.on('stack:changed', handleCanvasChange);
38
+ canvas.on('selection:created', handleCanvasChange);
39
+ canvas.on('selection:updated', handleCanvasChange);
40
+ canvas.on('selection:cleared', handleCanvasChange);
41
+
42
+ return () => {
43
+ canvas.off('object:added', handleCanvasChange);
44
+ canvas.off('object:removed', handleCanvasChange);
45
+ canvas.off('stack:changed', handleCanvasChange);
46
+ canvas.off('selection:created', handleCanvasChange);
47
+ canvas.off('selection:updated', handleCanvasChange);
48
+ canvas.off('selection:cleared', handleCanvasChange);
49
+ };
50
+ }
51
+ }, [canvas]);
52
+
53
+ const getLayerInfo = (object: fabric.Object): { name: string; icon: React.ReactElement } => {
54
+ if (object.type === 'i-text') {
55
+ return {
56
+ name: (object as fabric.IText).text?.substring(0, 20) || 'Text',
57
+ icon: <TextAlignCenter size="medium" />
58
+ };
59
+ }
60
+ if (object.type === 'image') {
61
+ const imageObject = object as fabric.Image;
62
+ const src = imageObject.getSrc();
63
+ return {
64
+ name: (object as any).name || (src ? src.split('/').pop()?.substring(0, 20) : undefined) || 'Image',
65
+ icon: <img src={src} alt="layer thumb" className="layer-thumbnail" />
66
+ };
67
+ }
68
+ return { name: 'Unknown Layer', icon: <></> };
69
+ };
70
+
71
+ const handleDragStart = (e: React.DragEvent<HTMLLIElement>, index: number) => {
72
+ if (isMobileView) return;
73
+ dragItemIndex.current = index;
74
+ e.dataTransfer.effectAllowed = 'move';
75
+ setTimeout(() => setIsDraggingOver(true), 0);
76
+ };
77
+
78
+ const handleDragEnterItem = (e: React.DragEvent<HTMLLIElement>, index: number) => {
79
+ if (isMobileView) return;
80
+ e.preventDefault();
81
+ dragOverItemIndex.current = index;
82
+ setDragOverIndexVisual(index);
83
+ };
84
+
85
+ const handleDragLeaveList = (e: React.DragEvent<HTMLUListElement>) => {
86
+ if (isMobileView) return;
87
+ if (!e.currentTarget.contains(e.relatedTarget as Node)) {
88
+ dragOverItemIndex.current = null;
89
+ setDragOverIndexVisual(null);
90
+ }
91
+ };
92
+
93
+ const handleDragOverList = (e: React.DragEvent<HTMLUListElement>) => {
94
+ if (isMobileView) return;
95
+ e.preventDefault();
96
+ e.dataTransfer.dropEffect = 'move';
97
+ };
98
+
99
+ const handleDrop = (e: React.DragEvent<HTMLUListElement>) => {
100
+ if (isMobileView) return;
101
+ e.preventDefault();
102
+ const sourceIndex = dragItemIndex.current;
103
+ const destinationIndex = dragOverItemIndex.current;
104
+
105
+ setIsDraggingOver(false);
106
+ setDragOverIndexVisual(null);
107
+ dragItemIndex.current = null;
108
+ dragOverItemIndex.current = null;
109
+
110
+ if (sourceIndex === null || destinationIndex === null || sourceIndex === destinationIndex) {
111
+ return;
112
+ }
113
+
114
+ const reorderedLayers = reorder(layers, sourceIndex, destinationIndex);
115
+
116
+ const objectToMove = layers[sourceIndex];
117
+
118
+ setLayers(reorderedLayers);
119
+
120
+ if (canvas && objectToMove) {
121
+ const totalObjects = canvas.getObjects().length;
122
+ const fabricTargetIndex = totalObjects - 1 - destinationIndex;
123
+ canvas.moveTo(objectToMove, fabricTargetIndex);
124
+ canvas.requestRenderAll();
125
+ }
126
+ };
127
+
128
+ const handleDragEnd = (e: React.DragEvent<HTMLLIElement>) => {
129
+ if (isMobileView) return;
130
+ setIsDraggingOver(false);
131
+ setDragOverIndexVisual(null);
132
+ dragItemIndex.current = null;
133
+ dragOverItemIndex.current = null;
134
+ };
135
+
136
+ const moveLayerUp = (index: number) => {
137
+ if (index <= 0 || !canvas) return;
138
+
139
+ const objectToMove = layers[index];
140
+ const objectAbove = layers[index - 1];
141
+
142
+ const fabricIndexOfAbove = canvas.getObjects().indexOf(objectAbove);
143
+ if (fabricIndexOfAbove < 0) {
144
+ console.error("Could not find objectAbove in canvas stack for moveLayerUp");
145
+ return;
146
+ }
147
+
148
+ const targetFabricIndex = fabricIndexOfAbove;
149
+ canvas.moveTo(objectToMove, targetFabricIndex);
150
+ setLayers(prevLayers => reorder(prevLayers, index, index - 1));
151
+ canvas.requestRenderAll();
152
+ };
153
+
154
+ const moveLayerDown = (index: number) => {
155
+ if (index >= layers.length - 1 || !canvas) return;
156
+
157
+ const objectToMove = layers[index];
158
+ const objectBelow = layers[index + 1];
159
+
160
+ const fabricIndexOfBelow = canvas.getObjects().indexOf(objectBelow);
161
+ if (fabricIndexOfBelow < 0) {
162
+ console.error("Could not find objectBelow in canvas stack for moveLayerDown");
163
+ return;
164
+ }
165
+
166
+ const targetFabricIndex = fabricIndexOfBelow;
167
+ canvas.moveTo(objectToMove, targetFabricIndex);
168
+ setLayers(prevLayers => reorder(prevLayers, index, index + 1));
169
+ canvas.requestRenderAll();
170
+ };
171
+
172
+ const handleLayerClick = (layer: fabric.Object) => {
173
+ selectObject(layer);
174
+ };
175
+
176
+ return (
177
+ <div className={`layer-panel`}>
178
+ <div className="layer-panel-header">
179
+ <h3>Layers</h3>
180
+ <button onClick={toggleLayerPanel} className="close-button" title="Close Panel">
181
+ <Close size="small" />
182
+ </button>
183
+ </div>
184
+ <ul
185
+ className={`layer-list ${isDraggingOver ? 'is-dragging-active' : ''}`}
186
+ onDragOver={handleDragOverList}
187
+ onDrop={handleDrop}
188
+ onDragLeave={handleDragLeaveList}
189
+ >
190
+ {layers.map((layer, index) => {
191
+ const layerInfo = getLayerInfo(layer);
192
+ const layerId = (layer as any).id;
193
+ const isSelected = layerId && layerId === selectedObjectId;
194
+ const isDraggingThisItem = !isMobileView && isDraggingOver && dragItemIndex.current === index;
195
+
196
+ let transformStyle = 'translateY(0px)';
197
+ if (!isMobileView && isDraggingOver && dragItemIndex.current !== null && dragOverIndexVisual !== null && !isDraggingThisItem) {
198
+ const draggingFromIndex = dragItemIndex.current;
199
+ const hoveringOverIndex = dragOverIndexVisual;
200
+
201
+ if (index > draggingFromIndex && index <= hoveringOverIndex) {
202
+ transformStyle = `translateY(-${LAYER_ITEM_HEIGHT_PX}px)`;
203
+ }
204
+ else if (index < draggingFromIndex && index >= hoveringOverIndex) {
205
+ transformStyle = `translateY(${LAYER_ITEM_HEIGHT_PX}px)`;
206
+ }
207
+ }
208
+
209
+ return (
210
+ <li
211
+ key={`layer-${layerId || index}`}
212
+ draggable={!isMobileView}
213
+ onDragStart={(e) => handleDragStart(e, index)}
214
+ onDragEnter={(e) => handleDragEnterItem(e, index)}
215
+ onDragEnd={handleDragEnd}
216
+ onClick={() => handleLayerClick(layer)}
217
+ className={`layer-item ${isSelected ? 'selected' : ''} ${isDraggingThisItem ? 'is-dragging-item' : ''}`}
218
+ style={{ transform: transformStyle }}
219
+ >
220
+ <span className="layer-icon">{layerInfo.icon}</span>
221
+ <span className="layer-name">{layerInfo.name}</span>
222
+
223
+ {isMobileView ? (
224
+ <div className="layer-mobile-controls">
225
+ <button
226
+ className="layer-move-button"
227
+ onClick={(e) => { e.stopPropagation(); moveLayerUp(index); }}
228
+ disabled={index === 0}
229
+ title="Move Up"
230
+ >
231
+ <Up size="small" />
232
+ </button>
233
+ <button
234
+ className="layer-move-button"
235
+ onClick={(e) => { e.stopPropagation(); moveLayerDown(index); }}
236
+ disabled={index === layers.length - 1}
237
+ title="Move Down"
238
+ >
239
+ <Down size="small" />
240
+ </button>
241
+ </div>
242
+ ) : (
243
+ <span className="layer-drag-indicator">
244
+ <Drag size="small" />
245
+ </span>
246
+ )}
247
+ </li>
248
+ );
249
+ })}
250
+ {layers.length === 0 && <li className="no-layers">No layers found</li>}
251
+ </ul>
252
+ </div>
253
+ );
254
+ };
255
+
256
+ export default LayerPanel;
@@ -0,0 +1,27 @@
1
+ import React from 'react';
2
+ import '../styles/LoadingOverlay.css';
3
+
4
+ interface LoadingOverlayProps {
5
+ isLoading: boolean;
6
+ message?: string;
7
+ className?: string;
8
+ }
9
+
10
+ const LoadingOverlay: React.FC<LoadingOverlayProps> = ({
11
+ isLoading,
12
+ message = "Loading Canvas...",
13
+ className = ""
14
+ }) => {
15
+ if (!isLoading) {
16
+ return null;
17
+ }
18
+
19
+ return (
20
+ <div className={`loading-overlay ${className}`}>
21
+ <div className="loading-spinner"></div>
22
+ <p>{message}</p>
23
+ </div>
24
+ );
25
+ };
26
+
27
+ export default LoadingOverlay;
@@ -0,0 +1,104 @@
1
+ import React from 'react';
2
+ import { useProductEditor } from '../context/ProductEditorContext';
3
+ import FloatingToolbar from './FloatingToolbar';
4
+ import ProductPreviews from './ProductPreviews';
5
+ import Toolbar from './Toolbar';
6
+ import LoadingOverlay from './LoadingOverlay';
7
+ import TextToolbar from './TextToolbar';
8
+ import LayerPanel from './LayerPanel';
9
+ import '../styles/ProductEditor.css';
10
+
11
+ const ProductEditor: React.FC = () => {
12
+ const {
13
+ canvasRef,
14
+ draftTemplates,
15
+ groupIndex,
16
+ handleTemplateChange,
17
+ isMobileView,
18
+ selectedTemplate,
19
+ showPreview,
20
+ inputName,
21
+ isCanvasLoading,
22
+ hookForm,
23
+ selectedTextObject,
24
+ showLayerPanel,
25
+ renderedDraftPreviews,
26
+ } = useProductEditor();
27
+
28
+ const disableCanvasEvents = (e: React.MouseEvent) => {
29
+ if ((e.target as HTMLElement).closest('.floating-toolbar') ||
30
+ (e.target as HTMLElement).closest('.bottom-preview-section') ||
31
+ (e.target as HTMLElement).closest('.mobile-bottom-toolbar')) {
32
+ return;
33
+ }
34
+ e.stopPropagation();
35
+ };
36
+
37
+ const getTemplatePreviewImage = (templateId?: number) => {
38
+ if (!templateId) return null;
39
+
40
+ const renderedPreview = renderedDraftPreviews?.find(preview => preview.templateId === templateId);
41
+ if (renderedPreview?.canvasPreview) {
42
+ return renderedPreview.canvasPreview;
43
+ }
44
+
45
+ return null;
46
+ };
47
+
48
+ const register = hookForm?.register;
49
+ return (
50
+ <div className="product-editor">
51
+ {register && (
52
+ <input
53
+ {...register(inputName)}
54
+ type='hidden'
55
+ />
56
+ )}
57
+ {!isCanvasLoading && draftTemplates.length > 0 && (
58
+ <div className="template-buttons">
59
+ {draftTemplates.map(({ template }) => (
60
+ <div
61
+ key={template.id}
62
+ className={`template-button ${selectedTemplate === template.id ? 'selected' : ''}`}
63
+ onClick={() => handleTemplateChange(template)}
64
+ >
65
+ {!isMobileView && (getTemplatePreviewImage(template.id) || (template.file && template.file.viewUrl)) && (
66
+ <img
67
+ src={getTemplatePreviewImage(template.id) || (template.file?.viewUrl || '')}
68
+ alt={template.name || `Template ${template.id}`}
69
+ className="template-thumbnail"
70
+ />
71
+ )}
72
+ <span className="template-name">
73
+ {template.name || `Template ${template.id}`}
74
+ </span>
75
+ </div>
76
+ ))}
77
+ </div>
78
+ )}
79
+ <div className="main-editor-layout" style={{ position: 'relative' }}>
80
+ <LoadingOverlay isLoading={isCanvasLoading} className="canvas-loading" />
81
+ <div
82
+ className={`editor-container ${isMobileView && showPreview ? 'has-bottom-padding' : ''}`}
83
+ >
84
+ {selectedTextObject && <TextToolbar />}
85
+ <div className="canvas-area" onClick={disableCanvasEvents}>
86
+ <canvas ref={canvasRef} />
87
+ </div>
88
+ {!isMobileView && !isCanvasLoading && <FloatingToolbar />}
89
+ {showLayerPanel && <LayerPanel />}
90
+ </div>
91
+
92
+ {showPreview && <ProductPreviews />}
93
+ </div>
94
+
95
+ {isMobileView && !isCanvasLoading && (
96
+ <div className="mobile-bottom-toolbar">
97
+ <Toolbar />
98
+ </div>
99
+ )}
100
+ </div>
101
+ );
102
+ };
103
+
104
+ export default ProductEditor;