@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.
- package/.turbo/turbo-build.log +1 -0
- package/README.md +174 -0
- package/dist/components/AlignMenu.d.ts +3 -0
- package/dist/components/AlignMenu.js +61 -0
- package/dist/components/FloatingToolbar.d.ts +4 -0
- package/dist/components/FloatingToolbar.js +97 -0
- package/dist/components/FormatButtons.d.ts +2 -0
- package/dist/components/FormatButtons.js +22 -0
- package/dist/components/ImageNavButton.d.ts +7 -0
- package/dist/components/ImageNavButton.js +14 -0
- package/dist/components/ImageZoomModal.d.ts +12 -0
- package/dist/components/ImageZoomModal.js +129 -0
- package/dist/components/LayerPanel.d.ts +4 -0
- package/dist/components/LayerPanel.js +223 -0
- package/dist/components/LoadingOverlay.d.ts +9 -0
- package/dist/components/LoadingOverlay.js +17 -0
- package/dist/components/ProductEditor.d.ts +4 -0
- package/dist/components/ProductEditor.js +67 -0
- package/dist/components/ProductImageGallery.d.ts +6 -0
- package/dist/components/ProductImageGallery.js +148 -0
- package/dist/components/ProductPreviews.d.ts +7 -0
- package/dist/components/ProductPreviews.js +281 -0
- package/dist/components/TextToolbar.d.ts +4 -0
- package/dist/components/TextToolbar.js +117 -0
- package/dist/components/Toolbar.d.ts +3 -0
- package/dist/components/Toolbar.js +47 -0
- package/dist/config/colorConfig.d.ts +1 -0
- package/dist/config/colorConfig.js +10 -0
- package/dist/config/fontConfig.d.ts +5 -0
- package/dist/config/fontConfig.js +21 -0
- package/dist/context/ProductEditorContext.d.ts +60 -0
- package/dist/context/ProductEditorContext.js +743 -0
- package/dist/hooks/useSwipeNavigate.d.ts +11 -0
- package/dist/hooks/useSwipeNavigate.js +35 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +30 -0
- package/dist/styles/BottomPreviewDisplay.css +65 -0
- package/dist/styles/FloatingToolbar.css +41 -0
- package/dist/styles/ImageGallery.css +147 -0
- package/dist/styles/ImageNavButton.css +35 -0
- package/dist/styles/ImageZoomModal.css +101 -0
- package/dist/styles/LayerPanel.css +179 -0
- package/dist/styles/LoadingOverlay.css +48 -0
- package/dist/styles/ProductEditor.css +192 -0
- package/dist/styles/TextToolbar.css +327 -0
- package/dist/styles/Toolbar.css +125 -0
- package/dist/styles/index.css +5 -0
- package/dist/styles/textToolbarSelectStyles.d.ts +3 -0
- package/dist/styles/textToolbarSelectStyles.js +30 -0
- package/dist/types.d.ts +228 -0
- package/dist/types.js +17 -0
- package/dist/utils/canvasAddText.d.ts +11 -0
- package/dist/utils/canvasAddText.js +66 -0
- package/dist/utils/canvasUtils.d.ts +37 -0
- package/dist/utils/canvasUtils.js +342 -0
- package/dist/utils/deviceUtils.d.ts +5 -0
- package/dist/utils/deviceUtils.js +33 -0
- package/dist/utils/draftTemplateUtils.d.ts +23 -0
- package/dist/utils/draftTemplateUtils.js +66 -0
- package/dist/utils/grid.d.ts +32 -0
- package/dist/utils/grid.js +151 -0
- package/dist/utils/imageUtils.d.ts +12 -0
- package/dist/utils/imageUtils.js +178 -0
- package/dist/utils/job.d.ts +40 -0
- package/dist/utils/job.js +115 -0
- package/dist/utils/keyboard.d.ts +1 -0
- package/dist/utils/keyboard.js +26 -0
- package/dist/utils/previewUtils.d.ts +9 -0
- package/dist/utils/previewUtils.js +39 -0
- package/dist/utils/psdConverter.d.ts +40 -0
- package/dist/utils/psdConverter.js +516 -0
- package/dist/utils/psdRenderUtils.d.ts +12 -0
- package/dist/utils/psdRenderUtils.js +465 -0
- package/dist/utils/renderUtils.d.ts +2 -0
- package/dist/utils/renderUtils.js +85 -0
- package/package.json +66 -0
- package/src/components/AlignMenu.tsx +49 -0
- package/src/components/FloatingToolbar.tsx +83 -0
- package/src/components/FormatButtons.tsx +31 -0
- package/src/components/ImageNavButton.tsx +23 -0
- package/src/components/ImageZoomModal.tsx +163 -0
- package/src/components/LayerPanel.tsx +256 -0
- package/src/components/LoadingOverlay.tsx +27 -0
- package/src/components/ProductEditor.tsx +104 -0
- package/src/components/ProductImageGallery.tsx +181 -0
- package/src/components/ProductPreviews.tsx +243 -0
- package/src/components/TextToolbar.tsx +164 -0
- package/src/components/Toolbar.tsx +86 -0
- package/src/config/colorConfig.ts +7 -0
- package/src/config/fontConfig.ts +23 -0
- package/src/context/ProductEditorContext.tsx +829 -0
- package/src/hooks/useSwipeNavigate.ts +40 -0
- package/src/index.ts +6 -0
- package/src/styles/BottomPreviewDisplay.css +65 -0
- package/src/styles/FloatingToolbar.css +41 -0
- package/src/styles/ImageGallery.css +147 -0
- package/src/styles/ImageNavButton.css +35 -0
- package/src/styles/ImageZoomModal.css +101 -0
- package/src/styles/LayerPanel.css +179 -0
- package/src/styles/LoadingOverlay.css +48 -0
- package/src/styles/ProductEditor.css +192 -0
- package/src/styles/TextToolbar.css +327 -0
- package/src/styles/Toolbar.css +125 -0
- package/src/styles/index.css +5 -0
- package/src/styles/textToolbarSelectStyles.ts +60 -0
- package/src/types/ag-psd/index.d.ts +1 -0
- package/src/types/fabric.d.ts +14 -0
- package/src/types/lodash/index.d.ts +1 -0
- package/src/types/psd.d.ts +53 -0
- package/src/types.ts +247 -0
- package/src/utils/canvasAddText.ts +72 -0
- package/src/utils/canvasUtils.ts +406 -0
- package/src/utils/deviceUtils.ts +31 -0
- package/src/utils/draftTemplateUtils.ts +70 -0
- package/src/utils/grid.ts +154 -0
- package/src/utils/imageUtils.ts +218 -0
- package/src/utils/job.ts +128 -0
- package/src/utils/keyboard.ts +28 -0
- package/src/utils/previewUtils.ts +43 -0
- package/src/utils/psdConverter.ts +595 -0
- package/src/utils/psdRenderUtils.ts +453 -0
- package/src/utils/renderUtils.ts +44 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { motion, AnimatePresence } from "framer-motion";
|
|
3
|
+
import ImageNavButton from "./ImageNavButton";
|
|
4
|
+
import ImageZoomModal from "./ImageZoomModal";
|
|
5
|
+
import { ZoomIn } from 'grommet-icons';
|
|
6
|
+
import { useProductEditor } from '../context/ProductEditorContext';
|
|
7
|
+
import { useSwipeNavigate } from '../hooks/useSwipeNavigate';
|
|
8
|
+
|
|
9
|
+
interface ImageGalleryProps {
|
|
10
|
+
fallbackImageUrl?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const ProductImageGallery: React.FC<ImageGalleryProps> = ({ fallbackImageUrl = '' }) => {
|
|
14
|
+
const { product } = useProductEditor();
|
|
15
|
+
const { featureImage = null, images = [], name: productName = 'Loading...' } = product;
|
|
16
|
+
const _images = featureImage ? [featureImage, ...images] : images;
|
|
17
|
+
const allImages = _images.map(image => {
|
|
18
|
+
if (!image) return { viewUrl: '/blueman_white.png' };
|
|
19
|
+
return { viewUrl: image.viewUrl };
|
|
20
|
+
});
|
|
21
|
+
const totalImages = allImages.length;
|
|
22
|
+
// image browser related states
|
|
23
|
+
const [slideDirection, setSlideDirection] = useState<'left' | 'right'>('right');
|
|
24
|
+
const [isZoomModalOpen, setIsZoomModalOpen] = useState(false);
|
|
25
|
+
const [isImageLoading, setIsImageLoading] = useState(true);
|
|
26
|
+
|
|
27
|
+
const [currentIndex, setCurrentIndex] = useState(0);
|
|
28
|
+
const [currentImageUrl, setCurrentImageUrl] = useState(
|
|
29
|
+
allImages[currentIndex]?.viewUrl || fallbackImageUrl || '/blueman_white.png'
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
// set current image URL (for normal image mode)
|
|
34
|
+
if (allImages && allImages.length > 0) {
|
|
35
|
+
setCurrentImageUrl(allImages[currentIndex]?.viewUrl || '/blueman_white.png');
|
|
36
|
+
}
|
|
37
|
+
}, [allImages, currentIndex, fallbackImageUrl]);
|
|
38
|
+
|
|
39
|
+
const handleNavigation = (newDirection: 'left' | 'right') => {
|
|
40
|
+
if (!allImages || allImages.length <= 1) return;
|
|
41
|
+
|
|
42
|
+
setSlideDirection(newDirection === 'left' ? 'left' : 'right');
|
|
43
|
+
|
|
44
|
+
let newIndex;
|
|
45
|
+
const actualTotalImages = Math.min(totalImages, allImages.length);
|
|
46
|
+
|
|
47
|
+
if (newDirection === 'left') {
|
|
48
|
+
newIndex = currentIndex === 0 ? actualTotalImages - 1 : currentIndex - 1;
|
|
49
|
+
} else {
|
|
50
|
+
newIndex = currentIndex === actualTotalImages - 1 ? 0 : currentIndex + 1;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
setCurrentIndex(newIndex);
|
|
54
|
+
setCurrentImageUrl(allImages[newIndex]?.viewUrl || '/blueman_white.png');
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const handleThumbnailClick = (index: number) => {
|
|
58
|
+
setCurrentIndex(index);
|
|
59
|
+
setCurrentImageUrl(allImages[index]?.viewUrl || '/blueman_white.png');
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Use the swipe hook
|
|
63
|
+
const swipeHandlers = useSwipeNavigate({
|
|
64
|
+
onSwipeLeft: () => handleNavigation('right'),
|
|
65
|
+
onSwipeRight: () => handleNavigation('left'),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const slideVariants = {
|
|
69
|
+
enter: (direction: 'left' | 'right') => ({
|
|
70
|
+
x: direction === 'left' ? -1000 : 1000,
|
|
71
|
+
opacity: 0
|
|
72
|
+
}),
|
|
73
|
+
center: {
|
|
74
|
+
zIndex: 1,
|
|
75
|
+
x: 0,
|
|
76
|
+
opacity: 1
|
|
77
|
+
},
|
|
78
|
+
exit: (direction: 'left' | 'right') => ({
|
|
79
|
+
zIndex: 0,
|
|
80
|
+
x: direction === 'left' ? 1000 : -1000,
|
|
81
|
+
opacity: 0
|
|
82
|
+
})
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<>
|
|
87
|
+
<div className="image-gallery-container relative">
|
|
88
|
+
<div
|
|
89
|
+
className="image-container overflow-hidden"
|
|
90
|
+
>
|
|
91
|
+
<AnimatePresence initial={false} custom={slideDirection}>
|
|
92
|
+
<motion.div
|
|
93
|
+
key={currentIndex}
|
|
94
|
+
custom={slideDirection}
|
|
95
|
+
variants={slideVariants}
|
|
96
|
+
initial="enter"
|
|
97
|
+
animate="center"
|
|
98
|
+
exit="exit"
|
|
99
|
+
transition={{
|
|
100
|
+
x: { type: "spring", stiffness: 300, damping: 30 },
|
|
101
|
+
opacity: { duration: 0.2 }
|
|
102
|
+
}}
|
|
103
|
+
className="image-slide"
|
|
104
|
+
{...swipeHandlers}
|
|
105
|
+
>
|
|
106
|
+
<img
|
|
107
|
+
src={currentImageUrl}
|
|
108
|
+
alt={productName || "Product Image"}
|
|
109
|
+
className={`gallery-image ${isImageLoading ? '' : 'image-loaded'}`}
|
|
110
|
+
onLoad={() => setIsImageLoading(false)}
|
|
111
|
+
/>
|
|
112
|
+
{isImageLoading && (
|
|
113
|
+
<div className="loading-indicator">
|
|
114
|
+
<div className="loading-spinner"></div>
|
|
115
|
+
</div>
|
|
116
|
+
)}
|
|
117
|
+
</motion.div>
|
|
118
|
+
</AnimatePresence>
|
|
119
|
+
|
|
120
|
+
<button
|
|
121
|
+
onClick={() => setIsZoomModalOpen(true)}
|
|
122
|
+
className="zoom-button"
|
|
123
|
+
type="button"
|
|
124
|
+
>
|
|
125
|
+
<ZoomIn className="zoom-icon" />
|
|
126
|
+
</button>
|
|
127
|
+
</div>
|
|
128
|
+
{allImages.length > 1 && (
|
|
129
|
+
<div className="nav-buttons-container">
|
|
130
|
+
<ImageNavButton
|
|
131
|
+
direction="left"
|
|
132
|
+
onClick={() => handleNavigation('left')}
|
|
133
|
+
/>
|
|
134
|
+
<ImageNavButton
|
|
135
|
+
direction="right"
|
|
136
|
+
onClick={() => handleNavigation('right')}
|
|
137
|
+
/>
|
|
138
|
+
</div>
|
|
139
|
+
)}
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
{allImages.length > 1 && (
|
|
143
|
+
<div className="thumbnails-container">
|
|
144
|
+
<div className="thumbnails-row">
|
|
145
|
+
{allImages.slice(0, 5).map((image, index) => {
|
|
146
|
+
const imageSrc = image.viewUrl || "/blueman_white.png";
|
|
147
|
+
const isSelected = index === currentIndex;
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<button
|
|
151
|
+
key={index}
|
|
152
|
+
onClick={() => handleThumbnailClick(index)}
|
|
153
|
+
className={`thumbnail-button ${isSelected ? 'selected' : ''}`}
|
|
154
|
+
type="button"
|
|
155
|
+
>
|
|
156
|
+
<img
|
|
157
|
+
src={imageSrc}
|
|
158
|
+
loading="lazy"
|
|
159
|
+
alt={`Product Image ${index + 1}`}
|
|
160
|
+
className="thumbnail-image"
|
|
161
|
+
/>
|
|
162
|
+
</button>
|
|
163
|
+
);
|
|
164
|
+
})}
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
)}
|
|
168
|
+
<ImageZoomModal
|
|
169
|
+
isOpen={isZoomModalOpen}
|
|
170
|
+
onClose={() => setIsZoomModalOpen(false)}
|
|
171
|
+
imageUrl={currentImageUrl}
|
|
172
|
+
productName={productName}
|
|
173
|
+
totalImages={allImages.length}
|
|
174
|
+
currentIndex={currentIndex}
|
|
175
|
+
allImages={allImages}
|
|
176
|
+
/>
|
|
177
|
+
</>
|
|
178
|
+
);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
export default ProductImageGallery;
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import React, { useEffect, useState, useRef } from 'react';
|
|
2
|
+
import ImageZoomModal from './ImageZoomModal';
|
|
3
|
+
import '../styles/ProductEditor.css';
|
|
4
|
+
import '../styles/BottomPreviewDisplay.css';
|
|
5
|
+
import { useProductEditor } from '../context/ProductEditorContext';
|
|
6
|
+
import { mapPreviewsWithRendered } from '../utils/draftTemplateUtils';
|
|
7
|
+
import { renderDraftPreviewsWithLayers } from '../utils/psdRenderUtils';
|
|
8
|
+
|
|
9
|
+
interface BottomPreviewDisplayProps { }
|
|
10
|
+
|
|
11
|
+
const ProductPreviews: React.FC<BottomPreviewDisplayProps> = () => {
|
|
12
|
+
const {
|
|
13
|
+
draftTemplates,
|
|
14
|
+
draftPreviews,
|
|
15
|
+
loadingPreviews,
|
|
16
|
+
renderedDraftPreviews,
|
|
17
|
+
} = useProductEditor();
|
|
18
|
+
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
19
|
+
const [selectedPreviewIndex, setSelectedPreviewIndex] = useState<number>(0);
|
|
20
|
+
const [renderedPreviews, setRenderedPreviews] = useState<{ draftPreviewId: number | undefined; pngDataUrl: string }[]>([]);
|
|
21
|
+
const [isProcessing, setIsProcessing] = useState(false);
|
|
22
|
+
|
|
23
|
+
// Use refs instead of state to track processed data without causing re-renders
|
|
24
|
+
const processedPreviewIdsRef = useRef<Set<number | undefined>>(new Set());
|
|
25
|
+
// Track which template versions we've already processed
|
|
26
|
+
const processedTemplateVersionsRef = useRef<Map<number, string>>(new Map());
|
|
27
|
+
|
|
28
|
+
// For debugging
|
|
29
|
+
const renderCountRef = useRef(0);
|
|
30
|
+
// Track if this is first mount
|
|
31
|
+
const isFirstMountRef = useRef(true);
|
|
32
|
+
|
|
33
|
+
// When mappedPreviews or renderedDraftPreviews changes, update our rendered PNGs
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
// Skip first render to avoid processing before renderedDraftPreviews are available
|
|
36
|
+
if (isFirstMountRef.current) {
|
|
37
|
+
isFirstMountRef.current = false;
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
renderCountRef.current++;
|
|
42
|
+
|
|
43
|
+
const processPreviewsWithLayers = async () => {
|
|
44
|
+
|
|
45
|
+
// Skip if we're already processing or if we don't have previews
|
|
46
|
+
if (isProcessing || !draftPreviews || draftPreviews.length === 0) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Map previews with their rendered layers
|
|
51
|
+
const mappedPreviews = mapPreviewsWithRendered(draftTemplates, draftPreviews, renderedDraftPreviews);
|
|
52
|
+
|
|
53
|
+
// Skip if there are no mappedPreviews to process
|
|
54
|
+
if (!mappedPreviews || mappedPreviews.length === 0) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Check if renderedDraftPreviews have changed compared to what we've processed before
|
|
59
|
+
let changedTemplateIds = new Set<number>();
|
|
60
|
+
renderedDraftPreviews.forEach(rdp => {
|
|
61
|
+
const templateId = rdp.templateId;
|
|
62
|
+
const templateVersion = rdp.draft; // Use the image data URL as a version identifier
|
|
63
|
+
const previousVersion = processedTemplateVersionsRef.current.get(templateId);
|
|
64
|
+
|
|
65
|
+
if (previousVersion !== templateVersion) {
|
|
66
|
+
changedTemplateIds.add(templateId);
|
|
67
|
+
// Update the stored version
|
|
68
|
+
processedTemplateVersionsRef.current.set(templateId, templateVersion);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Only process previews that haven't been processed yet or have changed
|
|
73
|
+
const previewsToProcess = mappedPreviews.filter(mp => {
|
|
74
|
+
const previewId = mp.draftPreview.id;
|
|
75
|
+
|
|
76
|
+
// Always process if we haven't processed this preview yet
|
|
77
|
+
if (!processedPreviewIdsRef.current.has(previewId)) {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// If no templates have changed, skip reprocessing
|
|
82
|
+
if (changedTemplateIds.size === 0) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Check if this preview uses any templates that have changed
|
|
87
|
+
const shouldProcess = mp.draftPreviewLayers.some(layer =>
|
|
88
|
+
layer.renderedLayer && changedTemplateIds.has(layer.renderedLayer.templateId));
|
|
89
|
+
|
|
90
|
+
return shouldProcess;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Skip if there's nothing new to process
|
|
94
|
+
if (previewsToProcess.length === 0) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
setIsProcessing(true);
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
// Process previews with our new function
|
|
102
|
+
const processedResults = await renderDraftPreviewsWithLayers(previewsToProcess);
|
|
103
|
+
|
|
104
|
+
// Update our rendered previews state
|
|
105
|
+
setRenderedPreviews(prevRendered => {
|
|
106
|
+
const newRendered = [...prevRendered];
|
|
107
|
+
|
|
108
|
+
processedResults.forEach(result => {
|
|
109
|
+
const existingIndex = newRendered.findIndex(
|
|
110
|
+
r => r.draftPreviewId === result.draftPreviewId
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
if (existingIndex !== -1) {
|
|
114
|
+
// Update existing preview
|
|
115
|
+
newRendered[existingIndex] = result;
|
|
116
|
+
} else {
|
|
117
|
+
// Add new preview
|
|
118
|
+
newRendered.push(result);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return newRendered;
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Update our processed IDs ref (not state)
|
|
126
|
+
processedResults.forEach(result => {
|
|
127
|
+
if (result.draftPreviewId !== undefined) {
|
|
128
|
+
processedPreviewIdsRef.current.add(result.draftPreviewId);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
} catch (error) {
|
|
132
|
+
console.error('Failed to process previews with layers:', error);
|
|
133
|
+
} finally {
|
|
134
|
+
setIsProcessing(false);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
processPreviewsWithLayers();
|
|
139
|
+
}, [draftTemplates, draftPreviews, renderedDraftPreviews, isProcessing]);
|
|
140
|
+
|
|
141
|
+
// One-time initialization to handle initial rendering of previews
|
|
142
|
+
// when component mounts and renderedDraftPreviews are available
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
if (draftPreviews?.length > 0 && renderedDraftPreviews?.length > 0 && renderedPreviews.length === 0) {
|
|
145
|
+
const mappedPreviews = mapPreviewsWithRendered(draftTemplates, draftPreviews, renderedDraftPreviews);
|
|
146
|
+
|
|
147
|
+
if (mappedPreviews?.length > 0) {
|
|
148
|
+
(async () => {
|
|
149
|
+
setIsProcessing(true);
|
|
150
|
+
try {
|
|
151
|
+
const processedResults = await renderDraftPreviewsWithLayers(mappedPreviews);
|
|
152
|
+
|
|
153
|
+
// Store initial results
|
|
154
|
+
setRenderedPreviews(processedResults);
|
|
155
|
+
|
|
156
|
+
// Mark these previews as processed
|
|
157
|
+
processedResults.forEach(result => {
|
|
158
|
+
if (result.draftPreviewId !== undefined) {
|
|
159
|
+
processedPreviewIdsRef.current.add(result.draftPreviewId);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Store initial template versions
|
|
164
|
+
renderedDraftPreviews.forEach(rdp => {
|
|
165
|
+
processedTemplateVersionsRef.current.set(rdp.templateId, rdp.draft);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
} catch (error) {
|
|
169
|
+
console.error('[Initialization] Error processing previews:', error);
|
|
170
|
+
} finally {
|
|
171
|
+
setIsProcessing(false);
|
|
172
|
+
}
|
|
173
|
+
})();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}, [draftPreviews, renderedDraftPreviews, draftTemplates, renderedPreviews.length]);
|
|
177
|
+
|
|
178
|
+
const openModal = (index: number) => {
|
|
179
|
+
setSelectedPreviewIndex(index);
|
|
180
|
+
setIsModalOpen(true);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const closeModal = () => {
|
|
184
|
+
setIsModalOpen(false);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// Determine the URL to display for each preview
|
|
188
|
+
const previewUrlsForDisplay = draftPreviews.length > 0
|
|
189
|
+
? draftPreviews.map(preview => {
|
|
190
|
+
// If we have a rendered PNG for this preview, use it
|
|
191
|
+
const renderedPreview = renderedPreviews.find(rp => rp.draftPreviewId === preview.id);
|
|
192
|
+
return renderedPreview?.pngDataUrl || '';
|
|
193
|
+
})
|
|
194
|
+
: renderedDraftPreviews.map(preview => {
|
|
195
|
+
// If we have a rendered PNG for this preview, use it
|
|
196
|
+
return preview.canvasPreview;
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const previews = draftPreviews?.length > 0 ? draftPreviews : renderedDraftPreviews;
|
|
200
|
+
return (
|
|
201
|
+
<>
|
|
202
|
+
{!isModalOpen && (
|
|
203
|
+
<div className="bottom-preview-section">
|
|
204
|
+
<div className="preview-images">
|
|
205
|
+
{previews.map((preview: any, index) => loadingPreviews || isProcessing ? (
|
|
206
|
+
<div key={`loading-${preview.id || index}`} className='preview-image-box'>
|
|
207
|
+
<div className="preview-image-box-loading-spinner" />
|
|
208
|
+
</div>
|
|
209
|
+
) : (
|
|
210
|
+
<div
|
|
211
|
+
key={preview.id || `preview-${index}`}
|
|
212
|
+
className="preview-image-box"
|
|
213
|
+
onClick={() => openModal(index)}
|
|
214
|
+
style={{
|
|
215
|
+
backgroundImage: `url(${previewUrlsForDisplay[index] || preview.file?.viewUrl || ''})`,
|
|
216
|
+
backgroundSize: 'contain',
|
|
217
|
+
backgroundRepeat: 'no-repeat',
|
|
218
|
+
backgroundPosition: 'center center'
|
|
219
|
+
}}
|
|
220
|
+
title={`Preview ${index + 1}`}
|
|
221
|
+
/>
|
|
222
|
+
))}
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
)}
|
|
226
|
+
|
|
227
|
+
<ImageZoomModal
|
|
228
|
+
isOpen={isModalOpen}
|
|
229
|
+
onClose={closeModal}
|
|
230
|
+
imageUrl={previewUrlsForDisplay[selectedPreviewIndex] || (previews[selectedPreviewIndex] as any)?.file?.viewUrl || ''}
|
|
231
|
+
productName="Preview"
|
|
232
|
+
totalImages={previews.length}
|
|
233
|
+
currentIndex={selectedPreviewIndex}
|
|
234
|
+
allImages={previews.map((preview: any, index) => ({
|
|
235
|
+
...preview,
|
|
236
|
+
key: preview.id || `modal-preview-${index}`
|
|
237
|
+
}))}
|
|
238
|
+
/>
|
|
239
|
+
</>
|
|
240
|
+
);
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
export default ProductPreviews;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect } from 'react';
|
|
2
|
+
import Select from 'react-select';
|
|
3
|
+
import { ChromePicker, ColorResult } from 'react-color';
|
|
4
|
+
import { useProductEditor } from '../context/ProductEditorContext';
|
|
5
|
+
import { FontOption } from '../config/fontConfig';
|
|
6
|
+
import { customStyles } from '../styles/textToolbarSelectStyles';
|
|
7
|
+
import '../styles/TextToolbar.css';
|
|
8
|
+
import { FormatButtons } from './FormatButtons';
|
|
9
|
+
import { AlignMenu } from './AlignMenu';
|
|
10
|
+
|
|
11
|
+
const TextToolbar: React.FC = () => {
|
|
12
|
+
const {
|
|
13
|
+
selectedTextObject,
|
|
14
|
+
updateSelectedText,
|
|
15
|
+
fontOptions,
|
|
16
|
+
colorPalette,
|
|
17
|
+
} = useProductEditor();
|
|
18
|
+
const [displayColorPicker, setDisplayColorPicker] = useState(false);
|
|
19
|
+
const [pickerMode, setPickerMode] = useState<'palette' | 'custom'>('palette');
|
|
20
|
+
const colorPickerRef = useRef<HTMLDivElement>(null);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
24
|
+
if (colorPickerRef.current && !colorPickerRef.current.contains(event.target as Node)) {
|
|
25
|
+
setDisplayColorPicker(false);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
29
|
+
return () => {
|
|
30
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
31
|
+
};
|
|
32
|
+
}, []);
|
|
33
|
+
|
|
34
|
+
if (!selectedTextObject || !updateSelectedText) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const currentFontValue = selectedTextObject.fontFamily || 'Nunito';
|
|
39
|
+
const currentColor = typeof selectedTextObject.fill === 'string' ? selectedTextObject.fill : '#000000';
|
|
40
|
+
|
|
41
|
+
const handleFontChange = (selectedOption: FontOption | null) => {
|
|
42
|
+
if (selectedOption) {
|
|
43
|
+
updateSelectedText({ fontFamily: selectedOption.value });
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const handlePaletteColorClick = (color: string) => {
|
|
48
|
+
updateSelectedText({ fill: color });
|
|
49
|
+
setDisplayColorPicker(false);
|
|
50
|
+
setPickerMode('palette');
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const handleCustomColorChange = (color: ColorResult) => {
|
|
54
|
+
updateSelectedText({ fill: color.hex });
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const handleSwatchClick = () => {
|
|
58
|
+
const currentlyVisible = displayColorPicker;
|
|
59
|
+
setDisplayColorPicker(!currentlyVisible);
|
|
60
|
+
if (!currentlyVisible) {
|
|
61
|
+
setPickerMode('palette');
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const handleCustomTriggerClick = () => {
|
|
66
|
+
setPickerMode('custom');
|
|
67
|
+
setDisplayColorPicker(true);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const currentFontOption = fontOptions.find(option => option.value === currentFontValue) || fontOptions[0];
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div className="text-toolbar">
|
|
74
|
+
<Select<FontOption>
|
|
75
|
+
options={fontOptions}
|
|
76
|
+
styles={customStyles}
|
|
77
|
+
value={currentFontOption}
|
|
78
|
+
onChange={handleFontChange}
|
|
79
|
+
isSearchable={true}
|
|
80
|
+
placeholder="Select or search font..."
|
|
81
|
+
className="react-select-container"
|
|
82
|
+
classNamePrefix="react-select"
|
|
83
|
+
/>
|
|
84
|
+
|
|
85
|
+
<div className="color-picker-trigger" onClick={handleSwatchClick} title="Change text color">
|
|
86
|
+
<div
|
|
87
|
+
className="color-picker-swatch"
|
|
88
|
+
style={{
|
|
89
|
+
backgroundColor: currentColor,
|
|
90
|
+
borderRadius: '50%',
|
|
91
|
+
width: '28px',
|
|
92
|
+
height: '28px',
|
|
93
|
+
minWidth: '28px',
|
|
94
|
+
minHeight: '28px',
|
|
95
|
+
flexShrink: 0
|
|
96
|
+
}}
|
|
97
|
+
/>
|
|
98
|
+
<span className="color-picker-label">Color</span>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
{displayColorPicker && (
|
|
102
|
+
<div className="color-picker-container" ref={colorPickerRef}>
|
|
103
|
+
<div className="color-picker-popover">
|
|
104
|
+
{pickerMode === 'palette' ? (
|
|
105
|
+
<div className="palette-grid">
|
|
106
|
+
{colorPalette.flat().map((color, index) => {
|
|
107
|
+
if (color === null) {
|
|
108
|
+
return (
|
|
109
|
+
<button
|
|
110
|
+
key="custom-trigger"
|
|
111
|
+
className="palette-trigger-swatch"
|
|
112
|
+
onClick={handleCustomTriggerClick}
|
|
113
|
+
title="Choose custom color"
|
|
114
|
+
>
|
|
115
|
+
+
|
|
116
|
+
</button>
|
|
117
|
+
);
|
|
118
|
+
} else {
|
|
119
|
+
return (
|
|
120
|
+
<button
|
|
121
|
+
key={`${color}-${index}`}
|
|
122
|
+
className="palette-swatch"
|
|
123
|
+
style={{ backgroundColor: color }}
|
|
124
|
+
onClick={() => handlePaletteColorClick(color)}
|
|
125
|
+
title={color}
|
|
126
|
+
/>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
})}
|
|
130
|
+
</div>
|
|
131
|
+
) : (
|
|
132
|
+
<>
|
|
133
|
+
<ChromePicker
|
|
134
|
+
color={currentColor}
|
|
135
|
+
onChange={handleCustomColorChange}
|
|
136
|
+
disableAlpha={true}
|
|
137
|
+
styles={{
|
|
138
|
+
default: {
|
|
139
|
+
picker: {
|
|
140
|
+
boxShadow: 'none',
|
|
141
|
+
width: '250px'
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}}
|
|
145
|
+
/>
|
|
146
|
+
<button
|
|
147
|
+
className="color-picker-back-btn"
|
|
148
|
+
onClick={() => setPickerMode('palette')}
|
|
149
|
+
>
|
|
150
|
+
Back to Palette
|
|
151
|
+
</button>
|
|
152
|
+
</>
|
|
153
|
+
)}
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
)}
|
|
157
|
+
|
|
158
|
+
<FormatButtons />
|
|
159
|
+
<AlignMenu />
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export default TextToolbar;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Apps, View, Trash, Layer } from 'grommet-icons';
|
|
3
|
+
import { useProductEditor } from '../context/ProductEditorContext';
|
|
4
|
+
import { addTextToCanvas } from '../utils/canvasUtils';
|
|
5
|
+
import '../styles/Toolbar.css';
|
|
6
|
+
|
|
7
|
+
export default function Toolbar() {
|
|
8
|
+
const {
|
|
9
|
+
canvas,
|
|
10
|
+
width,
|
|
11
|
+
setShowGrid,
|
|
12
|
+
showGrid,
|
|
13
|
+
height,
|
|
14
|
+
showPreview,
|
|
15
|
+
togglePreview,
|
|
16
|
+
showLayerPanel,
|
|
17
|
+
toggleLayerPanel,
|
|
18
|
+
deleteObject,
|
|
19
|
+
selectedObjectId,
|
|
20
|
+
renderedDraftPreviews
|
|
21
|
+
} = useProductEditor();
|
|
22
|
+
|
|
23
|
+
// Add handleAddText function before the return statement
|
|
24
|
+
const handleAddText = () => {
|
|
25
|
+
if (!canvas) return;
|
|
26
|
+
addTextToCanvas(canvas, width, height);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const toggleGrid = () => setShowGrid(!showGrid);
|
|
30
|
+
|
|
31
|
+
// Handle delete button click
|
|
32
|
+
const handleDelete = () => {
|
|
33
|
+
if (!canvas) return;
|
|
34
|
+
const activeObject = canvas.getActiveObject();
|
|
35
|
+
if (activeObject) {
|
|
36
|
+
deleteObject(activeObject);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Check if previews are available
|
|
41
|
+
const hasPreview = renderedDraftPreviews && renderedDraftPreviews.length > 0;
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<>
|
|
45
|
+
{/* Grid toggle button */}
|
|
46
|
+
<div className="grid-toggle">
|
|
47
|
+
<div
|
|
48
|
+
className={`toolbar-button ${showGrid ? 'active' : ''}`}
|
|
49
|
+
onClick={toggleGrid}
|
|
50
|
+
>
|
|
51
|
+
<Apps width={24} height={24} />
|
|
52
|
+
<span>{showGrid ? 'Hide Grid' : 'Show Grid'}</span>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
{/* Preview toggle button */}
|
|
57
|
+
<div className="preview-toggle">
|
|
58
|
+
<div
|
|
59
|
+
className={`toolbar-button ${showPreview && hasPreview ? 'active' : ''} ${!hasPreview ? 'disabled' : ''}`}
|
|
60
|
+
onClick={hasPreview ? togglePreview : undefined}
|
|
61
|
+
>
|
|
62
|
+
<View width={24} height={24} />
|
|
63
|
+
<span>{hasPreview && showPreview ? 'Hide Preview' : 'Show Preview'}</span>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
{/* Layer button */}
|
|
68
|
+
<div
|
|
69
|
+
className={`toolbar-button ${showLayerPanel ? 'active' : ''}`}
|
|
70
|
+
onClick={toggleLayerPanel}
|
|
71
|
+
>
|
|
72
|
+
<Layer width={24} height={24} />
|
|
73
|
+
<span>Layer</span>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
{/* Delete button */}
|
|
77
|
+
<div
|
|
78
|
+
className={`toolbar-button ${!selectedObjectId ? 'disabled' : ''}`}
|
|
79
|
+
onClick={handleDelete}
|
|
80
|
+
>
|
|
81
|
+
<Trash width={24} height={24} />
|
|
82
|
+
<span>Delete</span>
|
|
83
|
+
</div>
|
|
84
|
+
</>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const defaultPalette: (string | null)[][] = [
|
|
2
|
+
[null, '#000000', '#434343', '#666666', '#999999', '#CCCCCC', '#D9D9D9', '#EFEFEF'],
|
|
3
|
+
['#FFFFFF', '#F44E3B', '#FE9200', '#FCDC00', '#DBDF00', '#A4DD00', '#68CCCA', '#73D8FF'],
|
|
4
|
+
['#AEA1FF', '#FDA1FF', '#CF329A', '#FF6900', '#FCB900', '#7BDCB5', '#00D084', '#8ED1FC'],
|
|
5
|
+
['#0693E3', '#EB144C', '#F78DA7', '#9900EF', '#B80000', '#DB3E00', '#FCC400', '#27724D'],
|
|
6
|
+
['#607D8B', '#8BC34A', '#CDDC39', '#FFEB3B', '#FFC107', '#FF9800', '#FF5722', '#795548']
|
|
7
|
+
];
|