@jhits/plugin-images 0.0.4 → 0.0.6
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/package.json +27 -36
- package/src/api/resolve/route.ts +130 -11
- package/src/api/upload/index.ts +0 -4
- package/src/assets/noimagefound.jpg +0 -0
- package/src/components/BackgroundImage.tsx +25 -44
- package/src/components/GlobalImageEditor/config.ts +21 -0
- package/src/components/GlobalImageEditor/eventHandlers.ts +267 -0
- package/src/components/GlobalImageEditor/imageDetection.ts +160 -0
- package/src/components/GlobalImageEditor/imageSetup.ts +306 -0
- package/src/components/GlobalImageEditor/saveLogic.ts +133 -0
- package/src/components/GlobalImageEditor/stylingDetection.ts +122 -0
- package/src/components/GlobalImageEditor/transformParsing.ts +83 -0
- package/src/components/GlobalImageEditor/types.ts +39 -0
- package/src/components/GlobalImageEditor.tsx +186 -637
- package/src/components/Image.tsx +269 -103
- package/src/components/ImageBrowserModal.tsx +837 -0
- package/src/components/ImageEditor.tsx +323 -0
- package/src/components/ImageEffectsPanel.tsx +116 -0
- package/src/components/ImagePicker.tsx +208 -484
- package/src/components/index.ts +3 -0
- package/src/hooks/useImagePicker.ts +344 -0
- package/src/types/index.ts +24 -0
- package/src/utils/transforms.ts +54 -0
|
@@ -7,36 +7,31 @@
|
|
|
7
7
|
|
|
8
8
|
'use client';
|
|
9
9
|
|
|
10
|
-
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
|
10
|
+
import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react';
|
|
11
11
|
import { X, Edit2 } from 'lucide-react';
|
|
12
12
|
import { ImagePicker } from './ImagePicker';
|
|
13
13
|
import type { ImageMetadata } from '../types';
|
|
14
|
+
import type { SelectedImage, PendingTransform } from './GlobalImageEditor/types';
|
|
15
|
+
import { getPluginConfig } from './GlobalImageEditor/config';
|
|
16
|
+
import { parseImageData } from './GlobalImageEditor/imageDetection';
|
|
17
|
+
import { setupImageHandlers } from './GlobalImageEditor/imageSetup';
|
|
18
|
+
import { saveTransformToAPI, flushPendingSave, getFilename, normalizePosition } from './GlobalImageEditor/saveLogic';
|
|
19
|
+
import { handleImageChange, handleBrightnessChange, handleBlurChange } from './GlobalImageEditor/eventHandlers';
|
|
14
20
|
|
|
15
21
|
export function GlobalImageEditor() {
|
|
16
|
-
//
|
|
17
|
-
const config = useMemo(() =>
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const pluginProps = (window as any).__JHITS_PLUGIN_PROPS__?.['plugin-images'];
|
|
23
|
-
return {
|
|
24
|
-
enabled: pluginProps?.enabled !== undefined ? pluginProps.enabled : true,
|
|
25
|
-
className: pluginProps?.className,
|
|
26
|
-
overlayClassName: pluginProps?.overlayClassName,
|
|
27
|
-
};
|
|
28
|
-
}, []);
|
|
29
|
-
const [selectedImage, setSelectedImage] = useState<{
|
|
30
|
-
element: HTMLElement;
|
|
31
|
-
originalSrc: string;
|
|
32
|
-
brightness: number;
|
|
33
|
-
blur: number;
|
|
34
|
-
isBackground: boolean;
|
|
35
|
-
} | null>(null);
|
|
22
|
+
// Configuration
|
|
23
|
+
const config = useMemo(() => getPluginConfig(), []);
|
|
24
|
+
|
|
25
|
+
// State
|
|
26
|
+
const [selectedImage, setSelectedImage] = useState<SelectedImage | null>(null);
|
|
36
27
|
const [isOpen, setIsOpen] = useState(false);
|
|
37
28
|
const [userRole, setUserRole] = useState<string | null>(null);
|
|
38
29
|
const [isLoading, setIsLoading] = useState(true);
|
|
39
30
|
|
|
31
|
+
// Refs for save debouncing
|
|
32
|
+
const saveTransformTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
33
|
+
const pendingTransformRef = useRef<PendingTransform | null>(null);
|
|
34
|
+
|
|
40
35
|
// Check if user is admin/dev
|
|
41
36
|
useEffect(() => {
|
|
42
37
|
const checkUser = async () => {
|
|
@@ -63,13 +58,11 @@ export function GlobalImageEditor() {
|
|
|
63
58
|
const { id, currentBrightness, currentBlur } = e.detail || {};
|
|
64
59
|
if (!id) return;
|
|
65
60
|
|
|
66
|
-
console.log('[GlobalImageEditor] open-image-editor event received for id:', id);
|
|
67
61
|
|
|
68
62
|
// Find the element by data-image-id or data-background-image-id
|
|
69
63
|
let element = document.querySelector(`[data-image-id="${id}"], [data-background-image-id="${id}"]`) as HTMLElement;
|
|
70
64
|
|
|
71
65
|
if (!element) {
|
|
72
|
-
// Try finding by background image component
|
|
73
66
|
element = document.querySelector(`[data-background-image-component="true"][data-background-image-id="${id}"]`) as HTMLElement;
|
|
74
67
|
}
|
|
75
68
|
|
|
@@ -78,82 +71,27 @@ export function GlobalImageEditor() {
|
|
|
78
71
|
return;
|
|
79
72
|
}
|
|
80
73
|
|
|
81
|
-
// Determine if it's a background image
|
|
82
|
-
const isBackground = element.hasAttribute('data-background-image-component') ||
|
|
83
|
-
element.hasAttribute('data-background-image-id');
|
|
84
|
-
|
|
85
|
-
// Get current image source - handle Next.js Image wrapper
|
|
86
|
-
let currentSrc = '';
|
|
87
|
-
let actualImgElement: HTMLImageElement | null = null;
|
|
88
|
-
|
|
89
|
-
if (isBackground) {
|
|
90
|
-
// For background images, look for nested Image component
|
|
91
|
-
const imgWrapper = element.querySelector('[data-image-id]');
|
|
92
|
-
if (imgWrapper) {
|
|
93
|
-
actualImgElement = imgWrapper.querySelector('img') || null;
|
|
94
|
-
} else {
|
|
95
|
-
// Fallback: look for any img inside
|
|
96
|
-
actualImgElement = element.querySelector('img');
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (actualImgElement) {
|
|
100
|
-
currentSrc = actualImgElement.src;
|
|
101
|
-
} else {
|
|
102
|
-
// Try computed style as last resort
|
|
103
|
-
const bgImage = window.getComputedStyle(element).backgroundImage;
|
|
104
|
-
const urlMatch = bgImage.match(/url\(['"]?([^'"]+)['"]?\)/);
|
|
105
|
-
if (urlMatch && urlMatch[1]) {
|
|
106
|
-
currentSrc = urlMatch[1];
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
} else {
|
|
110
|
-
// For regular images, find the actual img element (Next.js wraps it)
|
|
111
|
-
if (element.tagName === 'IMG') {
|
|
112
|
-
actualImgElement = element as HTMLImageElement;
|
|
113
|
-
currentSrc = actualImgElement.src;
|
|
114
|
-
} else {
|
|
115
|
-
// Next.js Image wraps img in span/picture
|
|
116
|
-
actualImgElement = element.querySelector('img');
|
|
117
|
-
if (actualImgElement) {
|
|
118
|
-
currentSrc = actualImgElement.src;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (!currentSrc) {
|
|
124
|
-
console.error('[GlobalImageEditor] Could not determine image source for id:', id);
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
74
|
// Extract semantic ID
|
|
129
75
|
const semanticId = element.getAttribute('data-image-id') ||
|
|
130
76
|
element.getAttribute('data-background-image-id') ||
|
|
131
77
|
id;
|
|
132
78
|
|
|
133
|
-
//
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
console.log('[GlobalImageEditor] Opening editor for:', { id, semanticId, currentSrc, brightness, blur, isBackground });
|
|
141
|
-
|
|
142
|
-
setSelectedImage({
|
|
143
|
-
element,
|
|
144
|
-
originalSrc: currentSrc,
|
|
145
|
-
brightness,
|
|
146
|
-
blur,
|
|
147
|
-
isBackground,
|
|
148
|
-
});
|
|
79
|
+
// Parse image data
|
|
80
|
+
const imageData = parseImageData(element, semanticId, currentBrightness, currentBlur);
|
|
81
|
+
|
|
82
|
+
if (!imageData) {
|
|
83
|
+
console.error('[GlobalImageEditor] Failed to parse image data');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
149
86
|
|
|
150
87
|
// Store semantic ID on element if not already set
|
|
151
|
-
if (isBackground && !element.hasAttribute('data-background-image-id')) {
|
|
88
|
+
if (imageData.isBackground && !element.hasAttribute('data-background-image-id')) {
|
|
152
89
|
element.setAttribute('data-background-image-id', semanticId);
|
|
153
|
-
} else if (!isBackground && !element.hasAttribute('data-image-id')) {
|
|
90
|
+
} else if (!imageData.isBackground && !element.hasAttribute('data-image-id')) {
|
|
154
91
|
element.setAttribute('data-image-id', semanticId);
|
|
155
92
|
}
|
|
156
93
|
|
|
94
|
+
setSelectedImage(imageData);
|
|
157
95
|
setIsOpen(true);
|
|
158
96
|
};
|
|
159
97
|
|
|
@@ -167,561 +105,128 @@ export function GlobalImageEditor() {
|
|
|
167
105
|
// Add click handlers to all images
|
|
168
106
|
useEffect(() => {
|
|
169
107
|
if (isLoading || !userRole) {
|
|
170
|
-
console.log('[GlobalImageEditor] Skipping image handlers - isLoading:', isLoading, 'userRole:', userRole);
|
|
171
108
|
return;
|
|
172
109
|
}
|
|
173
110
|
|
|
174
|
-
console.log('[GlobalImageEditor] Setting up image handlers for role:', userRole);
|
|
175
|
-
|
|
176
|
-
// Declare cleanupFunctions outside setupImages so it's accessible in the cleanup function
|
|
177
|
-
const cleanupFunctions: Array<() => void> = [];
|
|
178
111
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const handleImageClick = (e: MouseEvent) => {
|
|
182
|
-
const target = e.target as HTMLElement;
|
|
183
|
-
|
|
184
|
-
// Only handle clicks on images that use the plugin components
|
|
185
|
-
// Check if it's an img element with data-image-id (from plugin Image component)
|
|
186
|
-
let img = target.closest('img[data-image-id]') as HTMLImageElement | null;
|
|
187
|
-
let isBackground = false;
|
|
188
|
-
let element: HTMLElement | null = null;
|
|
189
|
-
let currentSrc = '';
|
|
190
|
-
|
|
191
|
-
if (img) {
|
|
192
|
-
// This is a plugin Image component
|
|
193
|
-
element = img;
|
|
194
|
-
currentSrc = img.src;
|
|
195
|
-
} else {
|
|
196
|
-
// Check if it's an element with background-image
|
|
197
|
-
// First check for BackgroundImage component
|
|
198
|
-
let bgElement = target.closest('[data-background-image-component]') as HTMLElement | null;
|
|
199
|
-
|
|
200
|
-
if (bgElement) {
|
|
201
|
-
// This is a BackgroundImage component
|
|
202
|
-
element = bgElement;
|
|
203
|
-
isBackground = true;
|
|
204
|
-
|
|
205
|
-
// Try to get image source from nested Image component first
|
|
206
|
-
const imgInside = bgElement.querySelector('img[data-image-id]') as HTMLImageElement | null;
|
|
207
|
-
if (imgInside) {
|
|
208
|
-
currentSrc = imgInside.src;
|
|
209
|
-
} else {
|
|
210
|
-
// Fallback to background-image CSS
|
|
211
|
-
const bgImage = window.getComputedStyle(bgElement).backgroundImage;
|
|
212
|
-
const urlMatch = bgImage.match(/url\(['"]?([^'"]+)['"]?\)/);
|
|
213
|
-
if (urlMatch && urlMatch[1]) {
|
|
214
|
-
currentSrc = urlMatch[1];
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
if (!element || !currentSrc) return;
|
|
221
|
-
|
|
222
|
-
// Don't handle images in modals or the editor itself
|
|
223
|
-
if (element.closest('[data-image-editor]') || element.closest('[role="dialog"]')) {
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
e.preventDefault();
|
|
228
|
-
e.stopPropagation();
|
|
229
|
-
|
|
230
|
-
// Extract semantic ID from data attribute
|
|
231
|
-
let semanticId = element.getAttribute('data-image-id') || element.getAttribute('data-background-image-id');
|
|
232
|
-
|
|
233
|
-
// If not found, try to extract from the src URL
|
|
234
|
-
if (!semanticId && currentSrc.includes('/api/uploads/')) {
|
|
235
|
-
const urlPart = currentSrc.split('/api/uploads/')[1]?.split('?')[0];
|
|
236
|
-
// If it looks like a semantic ID (no extension or timestamp), use it
|
|
237
|
-
if (urlPart && !/^\d+-/.test(urlPart) && !/\.(jpg|jpeg|png|webp|gif|svg)$/i.test(urlPart)) {
|
|
238
|
-
semanticId = decodeURIComponent(urlPart);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// If still not found, generate one
|
|
243
|
-
if (!semanticId) {
|
|
244
|
-
semanticId = element.closest('[data-image-id]')?.getAttribute('data-image-id') ||
|
|
245
|
-
`image-${Date.now()}`;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Check for existing filter or data attributes
|
|
249
|
-
const filterStyle = element.style.filter || window.getComputedStyle(element).filter || '';
|
|
250
|
-
const dataBrightness = element.getAttribute('data-brightness');
|
|
251
|
-
const dataBlur = element.getAttribute('data-blur');
|
|
252
|
-
const currentBrightness = dataBrightness
|
|
253
|
-
? parseInt(dataBrightness)
|
|
254
|
-
: parseInt(filterStyle.match(/brightness\((\d+)%\)/)?.[1] || '100');
|
|
255
|
-
const currentBlur = dataBlur
|
|
256
|
-
? parseInt(dataBlur)
|
|
257
|
-
: parseInt(filterStyle.match(/blur\((\d+)px\)/)?.[1] || '0');
|
|
258
|
-
|
|
259
|
-
setSelectedImage({
|
|
260
|
-
element,
|
|
261
|
-
originalSrc: currentSrc,
|
|
262
|
-
brightness: currentBrightness,
|
|
263
|
-
blur: currentBlur,
|
|
264
|
-
isBackground,
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
// Store semantic ID on element for later use
|
|
268
|
-
if (isBackground) {
|
|
269
|
-
element.setAttribute('data-background-image-id', semanticId);
|
|
270
|
-
} else {
|
|
271
|
-
element.setAttribute('data-image-id', semanticId);
|
|
272
|
-
}
|
|
112
|
+
const cleanup = setupImageHandlers((imageData) => {
|
|
113
|
+
setSelectedImage(imageData);
|
|
273
114
|
setIsOpen(true);
|
|
274
|
-
};
|
|
275
|
-
|
|
276
|
-
// Add click listeners and hover effects to all images
|
|
277
|
-
// Only select images that use the plugin components (have data-image-id or are inside BackgroundImage)
|
|
278
|
-
const images = document.querySelectorAll('img[data-image-id]:not([data-no-edit])') as NodeListOf<HTMLImageElement>;
|
|
279
|
-
|
|
280
|
-
// Find elements with BackgroundImage components only
|
|
281
|
-
// Only BackgroundImage components should be editable, not all elements with background-image
|
|
282
|
-
const bgElements: HTMLElement[] = [];
|
|
283
|
-
const backgroundImageComponents = document.querySelectorAll('[data-background-image-component]');
|
|
284
|
-
backgroundImageComponents.forEach(el => {
|
|
285
|
-
bgElements.push(el as HTMLElement);
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
console.log('[GlobalImageEditor] Found', images.length, 'images and', bgElements.length, 'background images to make editable');
|
|
289
|
-
|
|
290
|
-
images.forEach((img, index) => {
|
|
291
|
-
console.log(`[GlobalImageEditor] Setting up image ${index + 1}:`, img.src);
|
|
292
|
-
// Store original position for cleanup
|
|
293
|
-
const originalPosition = img.style.position;
|
|
294
|
-
|
|
295
|
-
img.style.cursor = 'pointer';
|
|
296
|
-
img.setAttribute('data-editable-image', 'true');
|
|
297
|
-
img.addEventListener('click', handleImageClick as EventListener);
|
|
298
|
-
|
|
299
|
-
// Find the best container for the indicator
|
|
300
|
-
// Next.js Image wraps in span, so we need to find the right parent
|
|
301
|
-
let indicatorContainer: HTMLElement = img;
|
|
302
|
-
let parent = img.parentElement;
|
|
303
|
-
|
|
304
|
-
// Check if parent is a span (Next.js Image wrapper)
|
|
305
|
-
if (parent && (parent.tagName === 'SPAN' || parent.tagName === 'DIV')) {
|
|
306
|
-
const parentStyle = window.getComputedStyle(parent);
|
|
307
|
-
// Use parent if it has relative/absolute positioning or is a block element
|
|
308
|
-
if (parentStyle.position !== 'static' ||
|
|
309
|
-
(parentStyle.display !== 'inline' && parentStyle.display !== 'inline-block')) {
|
|
310
|
-
indicatorContainer = parent as HTMLElement;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// Ensure container has relative positioning
|
|
315
|
-
const containerStyle = window.getComputedStyle(indicatorContainer);
|
|
316
|
-
if (containerStyle.position === 'static') {
|
|
317
|
-
indicatorContainer.style.position = 'relative';
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// Get actual dimensions
|
|
321
|
-
const rect = indicatorContainer.getBoundingClientRect();
|
|
322
|
-
|
|
323
|
-
console.log(`[GlobalImageEditor] Using container for image ${index + 1}:`, indicatorContainer.tagName, `(${rect.width}x${rect.height})`);
|
|
324
|
-
|
|
325
|
-
const indicator = document.createElement('div');
|
|
326
|
-
indicator.className = 'image-edit-indicator';
|
|
327
|
-
indicator.setAttribute('data-image-index', index.toString());
|
|
328
|
-
indicator.style.cssText = `
|
|
329
|
-
position: absolute;
|
|
330
|
-
top: 0;
|
|
331
|
-
left: 0;
|
|
332
|
-
width: 100%;
|
|
333
|
-
height: 100%;
|
|
334
|
-
background: rgba(0, 0, 0, 0.4);
|
|
335
|
-
display: flex;
|
|
336
|
-
align-items: center;
|
|
337
|
-
justify-content: center;
|
|
338
|
-
opacity: 0;
|
|
339
|
-
transition: opacity 0.2s ease;
|
|
340
|
-
pointer-events: none;
|
|
341
|
-
z-index: 10000;
|
|
342
|
-
border-radius: inherit;
|
|
343
|
-
box-sizing: border-box;
|
|
344
|
-
`;
|
|
345
|
-
|
|
346
|
-
const label = document.createElement('div');
|
|
347
|
-
label.style.cssText = `
|
|
348
|
-
background: rgba(0, 0, 0, 0.9);
|
|
349
|
-
color: white;
|
|
350
|
-
padding: 8px 16px;
|
|
351
|
-
border-radius: 8px;
|
|
352
|
-
font-size: 12px;
|
|
353
|
-
font-weight: bold;
|
|
354
|
-
text-transform: uppercase;
|
|
355
|
-
letter-spacing: 0.5px;
|
|
356
|
-
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
357
|
-
`;
|
|
358
|
-
label.textContent = 'Click to Edit';
|
|
359
|
-
indicator.appendChild(label);
|
|
360
|
-
|
|
361
|
-
indicatorContainer.appendChild(indicator);
|
|
362
|
-
console.log(`[GlobalImageEditor] Added hover indicator to image ${index + 1}, container:`, indicatorContainer.tagName);
|
|
363
|
-
|
|
364
|
-
const addHoverEffect = () => {
|
|
365
|
-
console.log(`[GlobalImageEditor] Hover enter on image ${index + 1}`);
|
|
366
|
-
indicator.style.opacity = '1';
|
|
367
|
-
};
|
|
368
|
-
|
|
369
|
-
const removeHoverEffect = () => {
|
|
370
|
-
console.log(`[GlobalImageEditor] Hover leave on image ${index + 1}`);
|
|
371
|
-
indicator.style.opacity = '0';
|
|
372
|
-
};
|
|
373
|
-
|
|
374
|
-
// Add hover listeners to both image and container
|
|
375
|
-
img.addEventListener('mouseenter', addHoverEffect);
|
|
376
|
-
img.addEventListener('mouseleave', removeHoverEffect);
|
|
377
|
-
if (indicatorContainer !== img) {
|
|
378
|
-
indicatorContainer.addEventListener('mouseenter', addHoverEffect);
|
|
379
|
-
indicatorContainer.addEventListener('mouseleave', removeHoverEffect);
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
cleanupFunctions.push(() => {
|
|
383
|
-
img.removeEventListener('click', handleImageClick as EventListener);
|
|
384
|
-
img.removeEventListener('mouseenter', addHoverEffect);
|
|
385
|
-
img.removeEventListener('mouseleave', removeHoverEffect);
|
|
386
|
-
if (indicatorContainer !== img) {
|
|
387
|
-
indicatorContainer.removeEventListener('mouseenter', addHoverEffect);
|
|
388
|
-
indicatorContainer.removeEventListener('mouseleave', removeHoverEffect);
|
|
389
|
-
if (indicatorContainer.style.position === 'relative') {
|
|
390
|
-
indicatorContainer.style.position = '';
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
img.removeAttribute('data-editable-image');
|
|
394
|
-
img.style.cursor = '';
|
|
395
|
-
img.style.position = originalPosition;
|
|
396
|
-
indicator.remove();
|
|
397
|
-
});
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
// Process background images - show a button instead of hover overlay
|
|
401
|
-
bgElements.forEach((bgEl, index) => {
|
|
402
|
-
// Skip BackgroundImage components - they already have their own edit button
|
|
403
|
-
if (bgEl.hasAttribute('data-background-image-component')) {
|
|
404
|
-
console.log(`[GlobalImageEditor] Skipping BackgroundImage component ${index + 1} - it has its own edit button`);
|
|
405
|
-
return;
|
|
406
|
-
}
|
|
407
|
-
const originalPosition = bgEl.style.position;
|
|
408
|
-
if (!bgEl.style.position || bgEl.style.position === 'static') {
|
|
409
|
-
bgEl.style.position = 'relative';
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
bgEl.setAttribute('data-editable-background', 'true');
|
|
413
|
-
bgEl.setAttribute('data-background-image', 'true');
|
|
414
|
-
|
|
415
|
-
// Create a button indicator instead of hover overlay
|
|
416
|
-
const buttonContainer = document.createElement('div');
|
|
417
|
-
buttonContainer.className = 'background-edit-button-container';
|
|
418
|
-
buttonContainer.setAttribute('data-image-index', `bg-${index}`);
|
|
419
|
-
buttonContainer.style.cssText = `
|
|
420
|
-
position: absolute;
|
|
421
|
-
top: 12px;
|
|
422
|
-
right: 12px;
|
|
423
|
-
z-index: 10000;
|
|
424
|
-
pointer-events: auto;
|
|
425
|
-
`;
|
|
426
|
-
|
|
427
|
-
const editButton = document.createElement('button');
|
|
428
|
-
editButton.className = 'background-edit-button';
|
|
429
|
-
editButton.setAttribute('type', 'button');
|
|
430
|
-
editButton.style.cssText = `
|
|
431
|
-
background: rgba(0, 0, 0, 0.8);
|
|
432
|
-
color: white;
|
|
433
|
-
border: 2px solid rgba(255, 255, 255, 0.3);
|
|
434
|
-
padding: 10px 20px;
|
|
435
|
-
border-radius: 8px;
|
|
436
|
-
font-size: 12px;
|
|
437
|
-
font-weight: bold;
|
|
438
|
-
text-transform: uppercase;
|
|
439
|
-
letter-spacing: 0.5px;
|
|
440
|
-
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
|
441
|
-
cursor: pointer;
|
|
442
|
-
transition: all 0.2s ease;
|
|
443
|
-
display: flex;
|
|
444
|
-
align-items: center;
|
|
445
|
-
gap: 8px;
|
|
446
|
-
`;
|
|
447
|
-
editButton.innerHTML = `
|
|
448
|
-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
449
|
-
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
|
|
450
|
-
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
|
|
451
|
-
</svg>
|
|
452
|
-
Edit Background
|
|
453
|
-
`;
|
|
454
|
-
|
|
455
|
-
// Add hover effect to button
|
|
456
|
-
editButton.addEventListener('mouseenter', () => {
|
|
457
|
-
editButton.style.background = 'rgba(0, 0, 0, 0.95)';
|
|
458
|
-
editButton.style.borderColor = 'rgba(255, 255, 255, 0.5)';
|
|
459
|
-
editButton.style.transform = 'translateY(-2px)';
|
|
460
|
-
editButton.style.boxShadow = '0 6px 16px rgba(0, 0, 0, 0.5)';
|
|
461
|
-
});
|
|
462
|
-
|
|
463
|
-
editButton.addEventListener('mouseleave', () => {
|
|
464
|
-
editButton.style.background = 'rgba(0, 0, 0, 0.8)';
|
|
465
|
-
editButton.style.borderColor = 'rgba(255, 255, 255, 0.3)';
|
|
466
|
-
editButton.style.transform = 'translateY(0)';
|
|
467
|
-
editButton.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.4)';
|
|
468
|
-
});
|
|
469
|
-
|
|
470
|
-
// Handle button click
|
|
471
|
-
editButton.addEventListener('click', (e) => {
|
|
472
|
-
e.preventDefault();
|
|
473
|
-
e.stopPropagation();
|
|
474
|
-
handleImageClick(e as any);
|
|
475
|
-
});
|
|
476
|
-
|
|
477
|
-
buttonContainer.appendChild(editButton);
|
|
478
|
-
bgEl.appendChild(buttonContainer);
|
|
479
|
-
|
|
480
|
-
console.log(`[GlobalImageEditor] Added edit button to background image ${index + 1}`);
|
|
481
|
-
|
|
482
|
-
cleanupFunctions.push(() => {
|
|
483
|
-
bgEl.removeAttribute('data-editable-background');
|
|
484
|
-
bgEl.removeAttribute('data-background-image');
|
|
485
|
-
bgEl.style.position = originalPosition;
|
|
486
|
-
buttonContainer.remove();
|
|
487
|
-
});
|
|
488
115
|
});
|
|
489
116
|
|
|
490
|
-
}; // End of setupImages function
|
|
491
|
-
|
|
492
117
|
// Setup immediately and also after a short delay to catch dynamically loaded images
|
|
493
|
-
|
|
494
|
-
|
|
118
|
+
const timeoutId = setTimeout(() => {
|
|
119
|
+
const cleanup2 = setupImageHandlers((imageData) => {
|
|
120
|
+
setSelectedImage(imageData);
|
|
121
|
+
setIsOpen(true);
|
|
122
|
+
});
|
|
123
|
+
// Note: This creates a new cleanup, but the first one will handle the initial setup
|
|
124
|
+
}, 500);
|
|
495
125
|
|
|
496
126
|
return () => {
|
|
497
127
|
clearTimeout(timeoutId);
|
|
498
|
-
|
|
128
|
+
cleanup();
|
|
499
129
|
};
|
|
500
130
|
}, [isLoading, userRole]);
|
|
501
131
|
|
|
502
|
-
//
|
|
503
|
-
const
|
|
132
|
+
// Save image transform (scale/position) to API with debouncing
|
|
133
|
+
const saveImageTransformHandler = useCallback(async (
|
|
134
|
+
scale: number,
|
|
135
|
+
positionX: number,
|
|
136
|
+
positionY: number,
|
|
137
|
+
immediate: boolean = false,
|
|
138
|
+
brightnessOverride?: number,
|
|
139
|
+
blurOverride?: number
|
|
140
|
+
) => {
|
|
504
141
|
if (!selectedImage) return;
|
|
505
142
|
|
|
506
143
|
const { element } = selectedImage;
|
|
144
|
+
const semanticId = element.getAttribute('data-image-id') ||
|
|
145
|
+
element.getAttribute('data-background-image-id');
|
|
507
146
|
|
|
508
|
-
if (
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
147
|
+
if (!semanticId) return;
|
|
148
|
+
|
|
149
|
+
const filename = await getFilename(semanticId, selectedImage);
|
|
150
|
+
if (!filename) return;
|
|
151
|
+
|
|
152
|
+
// Store pending transform
|
|
153
|
+
pendingTransformRef.current = {
|
|
154
|
+
scale,
|
|
155
|
+
positionX,
|
|
156
|
+
positionY,
|
|
157
|
+
semanticId,
|
|
158
|
+
filename
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// Clear existing timeout
|
|
162
|
+
if (saveTransformTimeoutRef.current) {
|
|
163
|
+
clearTimeout(saveTransformTimeoutRef.current);
|
|
164
|
+
saveTransformTimeoutRef.current = null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// If immediate save is requested (e.g., when "Done" is clicked), save right away
|
|
168
|
+
if (immediate) {
|
|
169
|
+
const normalizedPositionX = normalizePosition(positionX);
|
|
170
|
+
const normalizedPositionY = normalizePosition(positionY);
|
|
171
|
+
|
|
172
|
+
// Use override values if provided, otherwise use selectedImage values
|
|
173
|
+
// IMPORTANT: Overrides contain the latest values from the editor
|
|
174
|
+
const finalBrightness = brightnessOverride !== undefined ? brightnessOverride : (selectedImage?.brightness ?? 100);
|
|
175
|
+
const finalBlur = blurOverride !== undefined ? blurOverride : (selectedImage?.blur ?? 0);
|
|
176
|
+
|
|
177
|
+
await saveTransformToAPI(
|
|
178
|
+
semanticId,
|
|
179
|
+
filename,
|
|
180
|
+
scale,
|
|
181
|
+
normalizedPositionX,
|
|
182
|
+
normalizedPositionY,
|
|
183
|
+
finalBrightness,
|
|
184
|
+
finalBlur
|
|
185
|
+
);
|
|
186
|
+
pendingTransformRef.current = null;
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Debounce: save after 300ms of no changes
|
|
191
|
+
saveTransformTimeoutRef.current = setTimeout(async () => {
|
|
192
|
+
const pending = pendingTransformRef.current;
|
|
193
|
+
if (pending && selectedImage) {
|
|
194
|
+
await flushPendingSave(pending, selectedImage);
|
|
546
195
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
if (img) {
|
|
556
|
-
img.src = image.url;
|
|
557
|
-
img.setAttribute('data-edited-src', image.url);
|
|
558
|
-
}
|
|
559
|
-
} else {
|
|
560
|
-
// Fallback: update background-image style if no nested Image component
|
|
561
|
-
const bgImage = window.getComputedStyle(element).backgroundImage;
|
|
562
|
-
if (bgImage && bgImage !== 'none') {
|
|
563
|
-
element.style.backgroundImage = `url(${image.url})`;
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
// Store attributes on the background container
|
|
568
|
-
element.setAttribute('data-background-image-id', semanticId);
|
|
569
|
-
element.setAttribute('data-edited-src', image.url);
|
|
570
|
-
element.setAttribute('data-brightness', selectedImage.brightness.toString());
|
|
571
|
-
element.setAttribute('data-blur', selectedImage.blur.toString());
|
|
572
|
-
|
|
573
|
-
// Apply filter to the wrapper element
|
|
574
|
-
if (selectedImage.brightness !== 100 || selectedImage.blur !== 0) {
|
|
575
|
-
element.style.filter = `brightness(${selectedImage.brightness}%) blur(${selectedImage.blur}px)`;
|
|
576
|
-
} else {
|
|
577
|
-
element.style.filter = '';
|
|
578
|
-
}
|
|
579
|
-
} else {
|
|
580
|
-
// For regular images, update the img element directly
|
|
581
|
-
if (element.tagName === 'IMG') {
|
|
582
|
-
(element as HTMLImageElement).src = image.url;
|
|
583
|
-
element.setAttribute('data-edited-src', image.url);
|
|
584
|
-
element.setAttribute('data-image-id', semanticId);
|
|
585
|
-
element.setAttribute('data-brightness', selectedImage.brightness.toString());
|
|
586
|
-
element.setAttribute('data-blur', selectedImage.blur.toString());
|
|
587
|
-
|
|
588
|
-
// Apply filter
|
|
589
|
-
if (selectedImage.brightness !== 100 || selectedImage.blur !== 0) {
|
|
590
|
-
element.style.filter = `brightness(${selectedImage.brightness}%) blur(${selectedImage.blur}px)`;
|
|
591
|
-
} else {
|
|
592
|
-
element.style.filter = '';
|
|
593
|
-
}
|
|
594
|
-
} else {
|
|
595
|
-
// Next.js Image wrapper - find the actual img element
|
|
596
|
-
const img = element.querySelector('img');
|
|
597
|
-
if (img) {
|
|
598
|
-
img.src = image.url;
|
|
599
|
-
img.setAttribute('data-edited-src', image.url);
|
|
600
|
-
}
|
|
601
|
-
element.setAttribute('data-image-id', semanticId);
|
|
602
|
-
element.setAttribute('data-edited-src', image.url);
|
|
603
|
-
element.setAttribute('data-brightness', selectedImage.brightness.toString());
|
|
604
|
-
element.setAttribute('data-blur', selectedImage.blur.toString());
|
|
605
|
-
|
|
606
|
-
// Apply filter to wrapper
|
|
607
|
-
if (selectedImage.brightness !== 100 || selectedImage.blur !== 0) {
|
|
608
|
-
element.style.filter = `brightness(${selectedImage.brightness}%) blur(${selectedImage.blur}px)`;
|
|
609
|
-
} else {
|
|
610
|
-
element.style.filter = '';
|
|
611
|
-
}
|
|
612
|
-
}
|
|
196
|
+
}, 300);
|
|
197
|
+
}, [selectedImage]);
|
|
198
|
+
|
|
199
|
+
// Cleanup timeout on unmount
|
|
200
|
+
useEffect(() => {
|
|
201
|
+
return () => {
|
|
202
|
+
if (saveTransformTimeoutRef.current) {
|
|
203
|
+
clearTimeout(saveTransformTimeoutRef.current);
|
|
613
204
|
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
window.dispatchEvent(new CustomEvent('image-mapping-updated', {
|
|
617
|
-
detail: { id: semanticId, filename, brightness: selectedImage.brightness, blur: selectedImage.blur }
|
|
618
|
-
}));
|
|
619
|
-
|
|
620
|
-
// Close the editor
|
|
621
|
-
handleClose();
|
|
622
|
-
}
|
|
623
|
-
};
|
|
205
|
+
};
|
|
206
|
+
}, []);
|
|
624
207
|
|
|
625
|
-
|
|
208
|
+
// Event handlers
|
|
209
|
+
const handleImageChangeWrapper = useCallback(async (image: ImageMetadata | null) => {
|
|
626
210
|
if (!selectedImage) return;
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
const semanticId = element.getAttribute('data-image-id') ||
|
|
630
|
-
element.getAttribute('data-background-image-id');
|
|
631
|
-
const currentFilter = element.style.filter || '';
|
|
632
|
-
const blurMatch = currentFilter.match(/blur\((\d+)px\)/);
|
|
633
|
-
const blur = blurMatch ? parseInt(blurMatch[1]) : selectedImage.blur;
|
|
634
|
-
|
|
635
|
-
// Update the selectedImage state
|
|
636
|
-
setSelectedImage({
|
|
637
|
-
...selectedImage,
|
|
638
|
-
brightness,
|
|
639
|
-
});
|
|
640
|
-
|
|
641
|
-
element.style.filter = `brightness(${brightness}%) blur(${blur}px)`;
|
|
642
|
-
element.setAttribute('data-brightness', brightness.toString());
|
|
643
|
-
|
|
644
|
-
// Save the effect immediately
|
|
645
|
-
if (semanticId) {
|
|
646
|
-
try {
|
|
647
|
-
let filename = semanticId;
|
|
648
|
-
if (selectedImage.isBackground) {
|
|
649
|
-
const bgImage = window.getComputedStyle(element).backgroundImage;
|
|
650
|
-
const urlMatch = bgImage.match(/url\(['"]?([^'"]+)['"]?\)/);
|
|
651
|
-
if (urlMatch && urlMatch[1]) {
|
|
652
|
-
filename = urlMatch[1].split('/api/uploads/')[1]?.split('?')[0] || semanticId;
|
|
653
|
-
}
|
|
654
|
-
} else {
|
|
655
|
-
filename = (element as HTMLImageElement).src.split('/api/uploads/')[1]?.split('?')[0] || semanticId;
|
|
656
|
-
}
|
|
657
|
-
await fetch('/api/plugin-images/resolve', {
|
|
658
|
-
method: 'POST',
|
|
659
|
-
headers: { 'Content-Type': 'application/json' },
|
|
660
|
-
body: JSON.stringify({
|
|
661
|
-
id: semanticId,
|
|
662
|
-
filename,
|
|
663
|
-
brightness,
|
|
664
|
-
blur,
|
|
665
|
-
}),
|
|
666
|
-
});
|
|
667
|
-
} catch (error) {
|
|
668
|
-
console.error('Failed to save brightness:', error);
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
};
|
|
211
|
+
await handleImageChange(image, selectedImage, handleClose);
|
|
212
|
+
}, [selectedImage]);
|
|
672
213
|
|
|
673
|
-
const
|
|
214
|
+
const handleBrightnessChangeWrapper = useCallback(async (brightness: number) => {
|
|
674
215
|
if (!selectedImage) return;
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
setSelectedImage({
|
|
685
|
-
...selectedImage,
|
|
686
|
-
blur,
|
|
687
|
-
});
|
|
688
|
-
|
|
689
|
-
element.style.filter = `brightness(${brightness}%) blur(${blur}px)`;
|
|
690
|
-
element.setAttribute('data-blur', blur.toString());
|
|
691
|
-
|
|
692
|
-
// Save the effect immediately
|
|
693
|
-
if (semanticId) {
|
|
694
|
-
try {
|
|
695
|
-
let filename = semanticId;
|
|
696
|
-
if (selectedImage.isBackground) {
|
|
697
|
-
const bgImage = window.getComputedStyle(element).backgroundImage;
|
|
698
|
-
const urlMatch = bgImage.match(/url\(['"]?([^'"]+)['"]?\)/);
|
|
699
|
-
if (urlMatch && urlMatch[1]) {
|
|
700
|
-
filename = urlMatch[1].split('/api/uploads/')[1]?.split('?')[0] || semanticId;
|
|
701
|
-
}
|
|
702
|
-
} else {
|
|
703
|
-
filename = (element as HTMLImageElement).src.split('/api/uploads/')[1]?.split('?')[0] || semanticId;
|
|
704
|
-
}
|
|
705
|
-
await fetch('/api/plugin-images/resolve', {
|
|
706
|
-
method: 'POST',
|
|
707
|
-
headers: { 'Content-Type': 'application/json' },
|
|
708
|
-
body: JSON.stringify({
|
|
709
|
-
id: semanticId,
|
|
710
|
-
filename,
|
|
711
|
-
brightness,
|
|
712
|
-
blur,
|
|
713
|
-
}),
|
|
714
|
-
});
|
|
715
|
-
} catch (error) {
|
|
716
|
-
console.error('Failed to save blur:', error);
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
};
|
|
216
|
+
// Don't save immediately when in editor - will be saved when "Done" is pressed
|
|
217
|
+
await handleBrightnessChange(brightness, selectedImage, setSelectedImage, false);
|
|
218
|
+
}, [selectedImage]);
|
|
219
|
+
|
|
220
|
+
const handleBlurChangeWrapper = useCallback(async (blur: number) => {
|
|
221
|
+
if (!selectedImage) return;
|
|
222
|
+
// Don't save immediately when in editor - will be saved when "Done" is pressed
|
|
223
|
+
await handleBlurChange(blur, selectedImage, setSelectedImage, false);
|
|
224
|
+
}, [selectedImage]);
|
|
720
225
|
|
|
721
|
-
const handleClose = () => {
|
|
226
|
+
const handleClose = useCallback(() => {
|
|
722
227
|
setIsOpen(false);
|
|
723
228
|
setSelectedImage(null);
|
|
724
|
-
};
|
|
229
|
+
}, []);
|
|
725
230
|
|
|
726
231
|
// Don't render if disabled or user is not admin/dev
|
|
727
232
|
if (!config.enabled || isLoading || !userRole) {
|
|
@@ -731,48 +236,92 @@ export function GlobalImageEditor() {
|
|
|
731
236
|
if (!isOpen || !selectedImage) {
|
|
732
237
|
return null;
|
|
733
238
|
}
|
|
734
|
-
|
|
239
|
+
|
|
735
240
|
return (
|
|
736
|
-
<div
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
onClick={
|
|
241
|
+
<div
|
|
242
|
+
className={`fixed inset-0 z-50 flex items-center justify-center ${config.overlayClassName || 'bg-black/50 backdrop-blur-sm'}`}
|
|
243
|
+
data-image-editor="true"
|
|
244
|
+
onClick={(e) => {
|
|
245
|
+
if (e.target === e.currentTarget) {
|
|
246
|
+
handleClose();
|
|
247
|
+
}
|
|
248
|
+
}}
|
|
740
249
|
>
|
|
741
|
-
<div
|
|
250
|
+
<div
|
|
251
|
+
className={`relative bg-white dark:bg-neutral-900 rounded-2xl shadow-2xl max-w-4xl w-full mx-4 max-h-[90vh] flex flex-col ${config.className || ''}`}
|
|
742
252
|
onClick={(e) => e.stopPropagation()}
|
|
743
|
-
className={config.className || "w-full max-w-2xl max-h-[90vh] bg-dashboard-card rounded-2xl border border-dashboard-border shadow-2xl m-4 overflow-y-auto"}
|
|
744
253
|
>
|
|
745
254
|
{/* Header */}
|
|
746
|
-
<div className="
|
|
747
|
-
<
|
|
748
|
-
<Edit2 size={20} className="text-neutral-600 dark:text-neutral-400" />
|
|
749
|
-
<h2 className="text-lg font-black uppercase tracking-tighter text-neutral-950 dark:text-white">
|
|
750
|
-
Edit Image
|
|
751
|
-
</h2>
|
|
752
|
-
</div>
|
|
255
|
+
<div className="flex items-center justify-between p-6 border-b dark:border-neutral-800">
|
|
256
|
+
<h2 className="text-2xl font-bold dark:text-white">Edit Image</h2>
|
|
753
257
|
<button
|
|
754
258
|
onClick={handleClose}
|
|
755
|
-
className="p-2 hover:bg-
|
|
259
|
+
className="p-2 hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-lg transition-colors"
|
|
260
|
+
aria-label="Close editor"
|
|
756
261
|
>
|
|
757
|
-
<X
|
|
262
|
+
<X className="w-5 h-5" />
|
|
758
263
|
</button>
|
|
759
264
|
</div>
|
|
760
265
|
|
|
761
266
|
{/* Content */}
|
|
762
267
|
<div className="p-6">
|
|
763
268
|
<ImagePicker
|
|
764
|
-
value={selectedImage.originalSrc}
|
|
765
|
-
onChange={
|
|
269
|
+
value={selectedImage.element.getAttribute('data-image-id') || selectedImage.element.getAttribute('data-background-image-id') || selectedImage.originalSrc}
|
|
270
|
+
onChange={handleImageChangeWrapper}
|
|
766
271
|
brightness={selectedImage.brightness}
|
|
767
272
|
blur={selectedImage.blur}
|
|
768
|
-
|
|
769
|
-
|
|
273
|
+
scale={selectedImage.scale}
|
|
274
|
+
positionX={selectedImage.positionX}
|
|
275
|
+
positionY={selectedImage.positionY}
|
|
276
|
+
onBrightnessChange={handleBrightnessChangeWrapper}
|
|
277
|
+
onBlurChange={handleBlurChangeWrapper}
|
|
278
|
+
onScaleChange={async (newScale: number) => {
|
|
279
|
+
if (selectedImage) {
|
|
280
|
+
const updated = { ...selectedImage, scale: newScale };
|
|
281
|
+
setSelectedImage(updated);
|
|
282
|
+
await saveImageTransformHandler(newScale, updated.positionX, updated.positionY);
|
|
283
|
+
}
|
|
284
|
+
}}
|
|
285
|
+
onPositionXChange={async (newPositionX: number) => {
|
|
286
|
+
if (selectedImage) {
|
|
287
|
+
const updated = { ...selectedImage, positionX: newPositionX };
|
|
288
|
+
setSelectedImage(updated);
|
|
289
|
+
await saveImageTransformHandler(updated.scale, newPositionX, updated.positionY);
|
|
290
|
+
}
|
|
291
|
+
}}
|
|
292
|
+
onPositionYChange={async (newPositionY: number) => {
|
|
293
|
+
if (selectedImage) {
|
|
294
|
+
const updated = { ...selectedImage, positionY: newPositionY };
|
|
295
|
+
setSelectedImage(updated);
|
|
296
|
+
await saveImageTransformHandler(updated.scale, updated.positionX, newPositionY);
|
|
297
|
+
}
|
|
298
|
+
}}
|
|
299
|
+
onEditorSave={async (finalScale: number, finalPositionX: number, finalPositionY: number, finalBrightness?: number, finalBlur?: number) => {
|
|
300
|
+
if (selectedImage) {
|
|
301
|
+
// Update selectedImage with final values
|
|
302
|
+
// Use provided brightness/blur if available, otherwise use current values
|
|
303
|
+
const updated = {
|
|
304
|
+
...selectedImage,
|
|
305
|
+
scale: finalScale,
|
|
306
|
+
positionX: finalPositionX,
|
|
307
|
+
positionY: finalPositionY,
|
|
308
|
+
brightness: finalBrightness !== undefined ? finalBrightness : selectedImage.brightness,
|
|
309
|
+
blur: finalBlur !== undefined ? finalBlur : selectedImage.blur
|
|
310
|
+
};
|
|
311
|
+
setSelectedImage(updated);
|
|
312
|
+
// Save with the final brightness and blur values
|
|
313
|
+
await saveImageTransformHandler(finalScale, finalPositionX, finalPositionY, true, updated.brightness, updated.blur);
|
|
314
|
+
}
|
|
315
|
+
}}
|
|
770
316
|
showEffects={true}
|
|
771
317
|
darkMode={false}
|
|
318
|
+
aspectRatio={selectedImage.aspectRatio}
|
|
319
|
+
borderRadius={selectedImage.borderRadius}
|
|
320
|
+
objectFit={selectedImage.objectFit}
|
|
321
|
+
objectPosition={selectedImage.objectPosition}
|
|
772
322
|
/>
|
|
773
323
|
</div>
|
|
774
324
|
</div>
|
|
775
325
|
</div>
|
|
776
326
|
);
|
|
777
327
|
}
|
|
778
|
-
|