@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,218 @@
1
+ import { fabric } from 'fabric';
2
+ import { DraftTemplate } from '../types';
3
+
4
+ /**
5
+ * Loads a template image onto a canvas with enhanced error handling and fallbacks
6
+ *
7
+ * @param fabricCanvas The Fabric.js canvas instance
8
+ * @param template The template containing image information
9
+ * @param width The canvas width
10
+ * @param height The canvas height
11
+ * @returns Promise that resolves when the image is loaded
12
+ */
13
+ export const loadRegularImagePromise = (
14
+ fabricCanvas: fabric.Canvas,
15
+ template: DraftTemplate,
16
+ width: number,
17
+ height: number,
18
+ ): Promise<void> => {
19
+ // Get the canvas ID for tracking if it exists
20
+ const canvasId = (fabricCanvas as any).loadingId;
21
+ const trackingEnabled = !!canvasId;
22
+
23
+ return new Promise((resolve) => {
24
+ // Check if canvas is still valid when tracking is enabled
25
+ if (trackingEnabled && (fabricCanvas as any).loadingId !== canvasId) {
26
+ resolve();
27
+ return;
28
+ }
29
+
30
+ // Make sure template has a file with a viewUrl
31
+ if (!template.file || !template.file.viewUrl) {
32
+ console.error('Template has no viewUrl:', template);
33
+ resolve();
34
+ return;
35
+ }
36
+
37
+ const imageUrl = template.file.viewUrl;
38
+
39
+ // Check if this might be a problematic file format
40
+ const isPossiblyUnsupported = !imageUrl.match(/\.(jpg|jpeg|png|gif|svg|webp)$/i);
41
+
42
+ // If we have a thumbnailUrl or previewUrl, use that instead for unsupported formats
43
+ let urlToUse = imageUrl;
44
+ if (isPossiblyUnsupported) {
45
+
46
+ // Try to use alternative URLs if available
47
+ if (template.file?.thumbnailUrl) {
48
+ urlToUse = template.file.thumbnailUrl;
49
+ } else if (template.file?.previewUrl) {
50
+ urlToUse = template.file.previewUrl;
51
+ }
52
+ }
53
+
54
+ // Add error handling for image loading
55
+ fabric.Image.fromURL(
56
+ urlToUse,
57
+ (img: fabric.Image) => {
58
+ // Check if canvas is still valid when tracking is enabled
59
+ if (trackingEnabled && (fabricCanvas as any).loadingId !== canvasId) {
60
+ console.warn('Canvas changed after image loaded, aborting');
61
+ resolve();
62
+ return;
63
+ }
64
+
65
+ if (!img || !img.width || !img.height) {
66
+ console.error('Failed to load image or image has invalid dimensions:', img);
67
+ // Try loading the image with a different approach - create an HTML image first
68
+ const imgElement = new Image();
69
+ imgElement.crossOrigin = 'anonymous';
70
+ imgElement.onload = () => {
71
+ // Check if canvas is still valid when tracking is enabled
72
+ if (trackingEnabled && (fabricCanvas as any).loadingId !== canvasId) {
73
+ console.warn('Canvas changed after alternative image loaded, aborting');
74
+ resolve();
75
+ return;
76
+ }
77
+
78
+ const fabricImg = new fabric.Image(imgElement);
79
+
80
+ // Continue with the same scaling and positioning logic
81
+ const scale = Math.min(
82
+ width / fabricImg.width!,
83
+ height / fabricImg.height!
84
+ );
85
+ fabricImg.scale(scale);
86
+
87
+ fabricImg.set({
88
+ left: (width - fabricImg.width! * scale) / 2,
89
+ top: (height - fabricImg.height! * scale) / 2,
90
+ selectable: false,
91
+ evented: false,
92
+ });
93
+
94
+ // Check again before adding to canvas when tracking is enabled
95
+ if (trackingEnabled && (fabricCanvas as any).loadingId !== canvasId) {
96
+ console.warn('Canvas changed before adding alternative image, aborting');
97
+ resolve();
98
+ return;
99
+ }
100
+
101
+ fabricCanvas.add(fabricImg);
102
+ fabricCanvas.sendToBack(fabricImg);
103
+
104
+ // Final check before render when tracking is enabled
105
+ if (trackingEnabled && (fabricCanvas as any).loadingId !== canvasId) {
106
+ console.warn('Canvas changed before rendering alternative image, aborting');
107
+ resolve();
108
+ return;
109
+ }
110
+
111
+ try {
112
+ fabricCanvas.renderAll();
113
+ } catch (e) {
114
+ console.error('Error rendering canvas with alternative image:', e);
115
+ }
116
+
117
+ resolve();
118
+ };
119
+
120
+ imgElement.onerror = () => {
121
+ // Check if canvas is still valid when tracking is enabled
122
+ if (trackingEnabled && (fabricCanvas as any).loadingId !== canvasId) {
123
+ console.warn('Canvas changed after image load error, aborting');
124
+ resolve();
125
+ return;
126
+ }
127
+
128
+ console.error('Alternative loading also failed. Using placeholder image.');
129
+ // Use a placeholder SVG as absolute fallback
130
+ const placeholderURL = 'data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%22800%22%20height%3D%22600%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20800%20600%22%20preserveAspectRatio%3D%22none%22%3E%3Cdefs%3E%3Cstyle%20type%3D%22text%2Fcss%22%3E%23holder_1891%20text%20%7Bfill%3A%23AAAAAA%3Bfont-weight%3Abold%3Bfont-family%3AArial%2C%20Helvetica%2C%20Open%20Sans%2C%20sans-serif%2C%20monospace%3Bfont-size%3A40pt%20%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cg%20id%3D%22holder%22%3E%3Crect%20width%3D%22800%22%20height%3D%22600%22%20fill%3D%22%23EEEEEE%22%3E%3C%2Frect%3E%3Cg%3E%3Ctext%20x%3D%22285%22%20y%3D%22300%22%3EThumbnail%20Unavailable%3C%2Ftext%3E%3Ctext%20x%3D%22205%22%20y%3D%22350%22%3ETemplate%3A%20' + template.name + '%3C%2Ftext%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fsvg%3E';
131
+ fabric.Image.fromURL(placeholderURL, (placeholderImg) => {
132
+ // Check if canvas is still valid when tracking is enabled
133
+ if (trackingEnabled && (fabricCanvas as any).loadingId !== canvasId) {
134
+ console.warn('Canvas changed before adding placeholder image, aborting');
135
+ resolve();
136
+ return;
137
+ }
138
+
139
+ placeholderImg.set({
140
+ left: 0,
141
+ top: 0,
142
+ width: width,
143
+ height: height,
144
+ selectable: false,
145
+ evented: false,
146
+ });
147
+ fabricCanvas.add(placeholderImg);
148
+
149
+ // Final check before render when tracking is enabled
150
+ if (trackingEnabled && (fabricCanvas as any).loadingId !== canvasId) {
151
+ console.warn('Canvas changed before rendering placeholder image, aborting');
152
+ resolve();
153
+ return;
154
+ }
155
+
156
+ try {
157
+ fabricCanvas.renderAll();
158
+ } catch (e) {
159
+ console.error('Error rendering canvas with placeholder image:', e);
160
+ }
161
+
162
+ resolve();
163
+ });
164
+ };
165
+
166
+ imgElement.src = urlToUse;
167
+ return;
168
+ }
169
+
170
+ // Scale image to fit canvas while maintaining aspect ratio
171
+ const scale = Math.min(
172
+ width / img.width!,
173
+ height / img.height!
174
+ );
175
+ img.scale(scale);
176
+
177
+
178
+ // Center the image
179
+ img.set({
180
+ left: (width - img.width! * scale) / 2,
181
+ top: (height - img.height! * scale) / 2,
182
+ selectable: false, // template image is not selectable
183
+ evented: false, // template image is not responsive to events
184
+ crossOrigin: 'anonymous',
185
+ });
186
+
187
+ // Check if canvas is still valid when tracking is enabled
188
+ if (trackingEnabled && (fabricCanvas as any).loadingId !== canvasId) {
189
+ console.warn('Canvas changed before adding image, aborting');
190
+ resolve();
191
+ return;
192
+ }
193
+
194
+ // Add the image to the canvas
195
+ fabricCanvas.add(img);
196
+
197
+ // Send the template image to the back
198
+ fabricCanvas.sendToBack(img);
199
+
200
+ // Final check before render when tracking is enabled
201
+ if (trackingEnabled && (fabricCanvas as any).loadingId !== canvasId) {
202
+ console.warn('Canvas changed before rendering, aborting');
203
+ resolve();
204
+ return;
205
+ }
206
+
207
+ try {
208
+ fabricCanvas.renderAll();
209
+ } catch (e) {
210
+ console.error('Error rendering canvas:', e);
211
+ }
212
+
213
+ resolve();
214
+ },
215
+ { crossOrigin: 'anonymous' }
216
+ );
217
+ });
218
+ };
@@ -0,0 +1,128 @@
1
+ import {
2
+ FieldType,
3
+ MerchiFile,
4
+ Product,
5
+ Variation,
6
+ VariationField,
7
+ VariationFieldsOption,
8
+ DraftTemplate,
9
+ DraftTemplateData,
10
+ } from '../types';
11
+
12
+ // Add a UUID generator for unique IDs
13
+ export const generateUniqueId = () => {
14
+ return 'canvas-obj-' + Math.random().toString(36).substring(2, 11) + Date.now().toString(36);
15
+ };
16
+
17
+ export function findTemplatesSelectedByVarations(draftTemplates: DraftTemplate[], variations: Variation[]) {
18
+ // filter all the selected variation values
19
+ const selectedVarationValues: string[] = variations
20
+ .filter((v: Variation) => v.value !== undefined && v.value !== null && v.value !== '')
21
+ .map((v: Variation) => String(v.value));
22
+
23
+ // return all the draft templates that have a selectedByVariationFieldOptions that includes
24
+ // any of the selected variation values
25
+ return draftTemplates.filter((template: DraftTemplate) => {
26
+ if (!template?.selectedByVariationFieldOptions) {
27
+ return false;
28
+ }
29
+ return !!template.selectedByVariationFieldOptions?.find(
30
+ (o: VariationFieldsOption) => selectedVarationValues.includes(String(o.id))
31
+ );
32
+ });
33
+ }
34
+
35
+ export function filterVariationsByTemplate(variations: Variation[], template: DraftTemplate) {
36
+ // filter the variations by the template and only return the variations which edit the template
37
+ // we will use these variations to add canvas objects to the canvas
38
+ const editFieldIds = template.editedByVariationFields?.map((field: VariationField) => field.id);
39
+ return variations.filter(
40
+ (v: Variation) => editFieldIds?.includes(v.variationField?.id)
41
+ );
42
+ }
43
+
44
+ export function buildVariationFieldCanvasObject(variation: Variation) {
45
+ // takes a variation and returns an associated canvas object to redner
46
+ const { value, variationField, variationFiles = [] } = variation;
47
+ const { fieldType = FieldType.TEXT_INPUT, id } = variationField || {};
48
+ if ([FieldType.TEXT_INPUT, FieldType.TEXT_AREA, FieldType.NUMBER_INPUT].includes(fieldType)) {
49
+ // if value is null or empty, do not create a text object
50
+ if (value === null || value === undefined || value === '') {
51
+ return {
52
+ fieldId: id,
53
+ value: null,
54
+ uniqueId: generateUniqueId()
55
+ };
56
+ }
57
+ // Used to add a text to the canvas
58
+ return [{
59
+ canvasObjectType: 'text',
60
+ value,
61
+ fieldId: id,
62
+ text: value,
63
+ fontSize: 24,
64
+ fontFamily: 'Nunito',
65
+ uniqueId: generateUniqueId(),
66
+ }];
67
+ }
68
+ if (fieldType === FieldType.FILE_UPLOAD) {
69
+ // Used to add an image to the canvas
70
+ return variationFiles.map((file: MerchiFile) => ({
71
+ canvasObjectType: 'image',
72
+ fieldId: id,
73
+ fileId: file.id,
74
+ value,
75
+ file: file,
76
+ uniqueId: generateUniqueId(),
77
+ }));
78
+ }
79
+ if ([FieldType.COLOUR_PICKER, FieldType.COLOUR_SELECT].includes(fieldType)) {
80
+ // Used to add a colour to the canvas
81
+ return [{
82
+ canvasObjectType: 'colour',
83
+ fieldId: id,
84
+ value,
85
+ colour: value,
86
+ uniqueId: generateUniqueId(),
87
+ }];
88
+ }
89
+ return [{
90
+ fieldId: id,
91
+ value,
92
+ uniqueId: generateUniqueId(),
93
+ }];
94
+ }
95
+
96
+ export function canvasTemplateVariationObjects(variations: Variation[], template: DraftTemplate) {
97
+ // takes a list of variations and a template and returns a list of canvas objects to render
98
+ const templateVariations = filterVariationsByTemplate(variations, template);
99
+
100
+ return templateVariations
101
+ .flatMap((v: Variation) => buildVariationFieldCanvasObject(v))
102
+ .filter((obj: any) => obj.canvasObjectType);
103
+ }
104
+
105
+ export function initDraftTemplates(variations: Variation[], product: Product): DraftTemplateData[] {
106
+ const { draftTemplates = [] } = product;
107
+
108
+ // Check for any templates that are selected by the variation options
109
+ const templates = findTemplatesSelectedByVarations(draftTemplates, variations) || [];
110
+
111
+ // If there are no templates selected, return the draft templates
112
+ const useTemplates = templates.length ? templates : draftTemplates;
113
+
114
+ return useTemplates.map((template: DraftTemplate) => {
115
+ // find the variations which edit the template
116
+ const templateVariations = filterVariationsByTemplate(variations, template);
117
+ // build the canvas objects for the template
118
+ const variationObjects = canvasTemplateVariationObjects(templateVariations, template);
119
+ return {
120
+ template,
121
+ variationFieldIds: templateVariations
122
+ .map((v: Variation) => v.variationField?.id)
123
+ .filter(Boolean)
124
+ .map(id => Number(id)),
125
+ variationObjects,
126
+ };
127
+ });
128
+ }
@@ -0,0 +1,28 @@
1
+ export const setupKeyboardEvents = (
2
+ canvas: fabric.Canvas,
3
+ onObjectRemoved?: (dataUrl: string) => void
4
+ ) => {
5
+ const handleKeyDown = (e: KeyboardEvent) => {
6
+ if (e.key === 'Delete') {
7
+ const activeObject = canvas.getActiveObject();
8
+ if (activeObject) {
9
+ canvas.remove(activeObject);
10
+ canvas.renderAll();
11
+
12
+ if (onObjectRemoved) {
13
+ const dataUrl = canvas.toDataURL({
14
+ format: 'png',
15
+ quality: 1,
16
+ });
17
+ onObjectRemoved(dataUrl);
18
+ }
19
+ }
20
+ }
21
+ };
22
+
23
+ document.addEventListener('keydown', handleKeyDown);
24
+
25
+ return () => {
26
+ document.removeEventListener('keydown', handleKeyDown);
27
+ };
28
+ };
@@ -0,0 +1,43 @@
1
+ import { DraftTemplateData, DraftPreview, DraftPreviewLayer } from '../types';
2
+
3
+ /**
4
+ * Gets all unique draft previews from the draft templates
5
+ * Loops through each template's draftPreviewLayers to collect associated previews
6
+ *
7
+ * @param draftTemplates Array of draft templates with their variation data
8
+ * @returns Array of unique draft previews
9
+ */
10
+ export function setNewDraftPreviews(draftTemplates: DraftTemplateData[]): DraftPreview[] {
11
+ // Create a map to track which previews we've already included by ID
12
+ const includedPreviewIds = new Set<number>();
13
+ const draftPreviews: DraftPreview[] = [];
14
+
15
+ // Loop through each draft template
16
+ draftTemplates.forEach((draftTemplateData) => {
17
+ const { template } = draftTemplateData;
18
+
19
+ // Skip if the template doesn't have preview layers
20
+ if (!template.draftPreviewLayers || template.draftPreviewLayers.length === 0) {
21
+ return;
22
+ }
23
+
24
+ // Look through each preview layer to find associated previews
25
+ template.draftPreviewLayers.forEach((layer: DraftPreviewLayer) => {
26
+ // If the layer has a preview and we haven't already included it
27
+ if (layer.draftPreview && layer.draftPreview.id) {
28
+ const previewId = layer.draftPreview.id;
29
+
30
+ if (!includedPreviewIds.has(previewId)) {
31
+ // Add to our tracking set
32
+ includedPreviewIds.add(previewId);
33
+
34
+ // Add the preview to our result array
35
+ draftPreviews.push(layer.draftPreview);
36
+ }
37
+ }
38
+ });
39
+ });
40
+
41
+ // Return the unique list of previews
42
+ return draftPreviews;
43
+ }