@jhits/plugin-images 0.0.3 → 0.0.5

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.
@@ -0,0 +1,374 @@
1
+ /**
2
+ * Global Image Editor Component
3
+ * Allows clicking on any image in the client app to edit it (admin/dev only)
4
+ *
5
+ * Reads configuration from window.__JHITS_PLUGIN_PROPS__['plugin-images']
6
+ */
7
+
8
+ 'use client';
9
+
10
+ import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react';
11
+ import { X, Edit2 } from 'lucide-react';
12
+ import { ImagePicker } from '../ImagePicker';
13
+ import type { ImageMetadata } from '../../types';
14
+ import type { SelectedImage, PendingTransform } from './types';
15
+ import { getPluginConfig } from './config';
16
+ import { parseImageData } from './imageDetection';
17
+ import { setupImageHandlers } from './imageSetup';
18
+ import { saveTransformToAPI, flushPendingSave, getFilename, normalizePosition } from './saveLogic';
19
+ import { handleImageChange, handleBrightnessChange, handleBlurChange } from './eventHandlers';
20
+
21
+ export function GlobalImageEditor() {
22
+ // Configuration
23
+ const config = useMemo(() => getPluginConfig(), []);
24
+
25
+ // State
26
+ const [selectedImage, setSelectedImage] = useState<SelectedImage | null>(null);
27
+ const [isOpen, setIsOpen] = useState(false);
28
+ const [userRole, setUserRole] = useState<string | null>(null);
29
+ const [isLoading, setIsLoading] = useState(true);
30
+
31
+ // Refs for save debouncing
32
+ const saveTransformTimeoutRef = useRef<NodeJS.Timeout | null>(null);
33
+ const pendingTransformRef = useRef<PendingTransform | null>(null);
34
+
35
+ // Check if user is admin/dev
36
+ useEffect(() => {
37
+ const checkUser = async () => {
38
+ try {
39
+ const res = await fetch('/api/me');
40
+ const data = await res.json();
41
+ if (data.loggedIn && (data.user?.role === 'admin' || data.user?.role === 'dev')) {
42
+ setUserRole(data.user.role);
43
+ }
44
+ } catch (error) {
45
+ console.error('Failed to check user role:', error);
46
+ } finally {
47
+ setIsLoading(false);
48
+ }
49
+ };
50
+ checkUser();
51
+ }, []);
52
+
53
+ // Listen for open-image-editor custom event from Image/BackgroundImage components
54
+ useEffect(() => {
55
+ if (!userRole) return;
56
+
57
+ const handleOpenEditor = async (e: CustomEvent) => {
58
+ const { id, currentBrightness, currentBlur } = e.detail || {};
59
+ if (!id) return;
60
+
61
+ console.log('[GlobalImageEditor] open-image-editor event received for id:', id);
62
+
63
+ // Find the element by data-image-id or data-background-image-id
64
+ let element = document.querySelector(`[data-image-id="${id}"], [data-background-image-id="${id}"]`) as HTMLElement;
65
+
66
+ if (!element) {
67
+ element = document.querySelector(`[data-background-image-component="true"][data-background-image-id="${id}"]`) as HTMLElement;
68
+ }
69
+
70
+ if (!element) {
71
+ console.error('[GlobalImageEditor] Element not found for id:', id);
72
+ return;
73
+ }
74
+
75
+ // Extract semantic ID
76
+ const semanticId = element.getAttribute('data-image-id') ||
77
+ element.getAttribute('data-background-image-id') ||
78
+ id;
79
+
80
+ // Parse image data
81
+ const imageData = parseImageData(element, semanticId, currentBrightness, currentBlur);
82
+
83
+ if (!imageData) {
84
+ console.error('[GlobalImageEditor] Failed to parse image data');
85
+ return;
86
+ }
87
+
88
+ // Store semantic ID on element if not already set
89
+ if (imageData.isBackground && !element.hasAttribute('data-background-image-id')) {
90
+ element.setAttribute('data-background-image-id', semanticId);
91
+ } else if (!imageData.isBackground && !element.hasAttribute('data-image-id')) {
92
+ element.setAttribute('data-image-id', semanticId);
93
+ }
94
+
95
+ setSelectedImage(imageData);
96
+ setIsOpen(true);
97
+ };
98
+
99
+ window.addEventListener('open-image-editor', handleOpenEditor as unknown as EventListener);
100
+
101
+ return () => {
102
+ window.removeEventListener('open-image-editor', handleOpenEditor as unknown as EventListener);
103
+ };
104
+ }, [userRole]);
105
+
106
+ // Add click handlers to all images
107
+ useEffect(() => {
108
+ if (isLoading || !userRole) {
109
+ console.log('[GlobalImageEditor] Skipping image handlers - isLoading:', isLoading, 'userRole:', userRole);
110
+ return;
111
+ }
112
+
113
+ console.log('[GlobalImageEditor] Setting up image handlers for role:', userRole);
114
+
115
+ const cleanup = setupImageHandlers((imageData) => {
116
+ setSelectedImage(imageData);
117
+ setIsOpen(true);
118
+ });
119
+
120
+ // Setup immediately and also after a short delay to catch dynamically loaded images
121
+ const timeoutId = setTimeout(() => {
122
+ const cleanup2 = setupImageHandlers((imageData) => {
123
+ setSelectedImage(imageData);
124
+ setIsOpen(true);
125
+ });
126
+ // Note: This creates a new cleanup, but the first one will handle the initial setup
127
+ }, 500);
128
+
129
+ return () => {
130
+ clearTimeout(timeoutId);
131
+ cleanup();
132
+ };
133
+ }, [isLoading, userRole]);
134
+
135
+ // Save image transform (scale/position) to API with debouncing
136
+ const saveImageTransformHandler = useCallback(async (
137
+ scale: number,
138
+ positionX: number,
139
+ positionY: number,
140
+ immediate: boolean = false,
141
+ brightnessOverride?: number,
142
+ blurOverride?: number
143
+ ) => {
144
+ if (!selectedImage) return;
145
+
146
+ const { element } = selectedImage;
147
+ const semanticId = element.getAttribute('data-image-id') ||
148
+ element.getAttribute('data-background-image-id');
149
+
150
+ if (!semanticId) return;
151
+
152
+ const filename = await getFilename(semanticId, selectedImage);
153
+ if (!filename) {
154
+ console.error('[GlobalImageEditor] getFilename returned null for semanticId:', semanticId);
155
+ return;
156
+ }
157
+ console.log('[GlobalImageEditor] getFilename returned:', filename, 'for semanticId:', semanticId);
158
+
159
+ // Use override values if provided, otherwise use selectedImage values
160
+ // IMPORTANT: Overrides contain the latest values from the editor
161
+ const finalBrightness = brightnessOverride !== undefined ? brightnessOverride : (selectedImage?.brightness ?? 100);
162
+ const finalBlur = blurOverride !== undefined ? blurOverride : (selectedImage?.blur ?? 0);
163
+
164
+ // Store pending transform
165
+ pendingTransformRef.current = {
166
+ scale,
167
+ positionX,
168
+ positionY,
169
+ semanticId,
170
+ filename
171
+ };
172
+
173
+ // Clear existing timeout
174
+ if (saveTransformTimeoutRef.current) {
175
+ clearTimeout(saveTransformTimeoutRef.current);
176
+ saveTransformTimeoutRef.current = null;
177
+ }
178
+
179
+ // If immediate save is requested (e.g., when "Done" is clicked), save right away
180
+ if (immediate) {
181
+ const normalizedPositionX = normalizePosition(positionX);
182
+ const normalizedPositionY = normalizePosition(positionY);
183
+
184
+ console.log('[GlobalImageEditor] Immediate save to API:', { semanticId, filename, scale, normalizedPositionX, normalizedPositionY, finalBrightness, finalBlur });
185
+ await saveTransformToAPI(
186
+ semanticId,
187
+ filename,
188
+ scale,
189
+ normalizedPositionX,
190
+ normalizedPositionY,
191
+ finalBrightness,
192
+ finalBlur
193
+ );
194
+ pendingTransformRef.current = null;
195
+ return;
196
+ }
197
+
198
+ // Debounce: save after 300ms of no changes
199
+ saveTransformTimeoutRef.current = setTimeout(async () => {
200
+ const pending = pendingTransformRef.current;
201
+ if (pending && selectedImage) {
202
+ await flushPendingSave(pending, selectedImage);
203
+ }
204
+ }, 300);
205
+ }, [selectedImage]);
206
+
207
+ // Cleanup timeout on unmount
208
+ useEffect(() => {
209
+ return () => {
210
+ if (saveTransformTimeoutRef.current) {
211
+ clearTimeout(saveTransformTimeoutRef.current);
212
+ }
213
+ };
214
+ }, []);
215
+
216
+ // Event handlers
217
+ const handleImageChangeWrapper = useCallback(async (image: ImageMetadata | null) => {
218
+ if (!selectedImage) return;
219
+ await handleImageChange(image, selectedImage, handleClose);
220
+ }, [selectedImage]);
221
+
222
+ const handleBrightnessChangeWrapper = useCallback(async (brightness: number) => {
223
+ if (!selectedImage) return;
224
+ // Don't save immediately when in editor - will be saved when "Done" is pressed via onEditorSave
225
+ await handleBrightnessChange(brightness, selectedImage, setSelectedImage, false);
226
+ }, [selectedImage]);
227
+
228
+ const handleBlurChangeWrapper = useCallback(async (blur: number) => {
229
+ if (!selectedImage) return;
230
+ // Don't save immediately when in editor - will be saved when "Done" is pressed via onEditorSave
231
+ await handleBlurChange(blur, selectedImage, setSelectedImage, false);
232
+ }, [selectedImage]);
233
+
234
+ const handleEditorSave = useCallback(async (
235
+ finalScale: number,
236
+ finalPositionX: number,
237
+ finalPositionY: number,
238
+ finalBrightness?: number,
239
+ finalBlur?: number
240
+ ) => {
241
+ console.log('[GlobalImageEditor] handleEditorSave callback created/updated');
242
+ console.log('[GlobalImageEditor] handleEditorSave called:', { finalScale, finalPositionX, finalPositionY, finalBrightness, finalBlur, hasSelectedImage: !!selectedImage });
243
+ if (selectedImage) {
244
+ // Use provided values if they're not undefined, otherwise fall back to selectedImage values
245
+ // Note: 0 is a valid value for blur, so we check for undefined explicitly
246
+ const brightness = finalBrightness !== undefined ? finalBrightness : selectedImage.brightness;
247
+ const blur = finalBlur !== undefined ? finalBlur : selectedImage.blur;
248
+
249
+ const updated = {
250
+ ...selectedImage,
251
+ scale: finalScale,
252
+ positionX: finalPositionX,
253
+ positionY: finalPositionY,
254
+ brightness,
255
+ blur,
256
+ };
257
+ setSelectedImage(updated);
258
+
259
+ console.log('[GlobalImageEditor] Calling saveImageTransformHandler with:', { finalScale, finalPositionX, finalPositionY, brightness, blur, immediate: true });
260
+ // Only save once when editor closes with all final values
261
+ // ImagePicker prevents duplicate calls by not calling individual handlers when onEditorSave is provided
262
+ await saveImageTransformHandler(
263
+ finalScale,
264
+ finalPositionX,
265
+ finalPositionY,
266
+ true,
267
+ brightness,
268
+ blur
269
+ );
270
+ } else {
271
+ console.error('[GlobalImageEditor] handleEditorSave called but selectedImage is null!');
272
+ }
273
+ }, [selectedImage, saveImageTransformHandler]);
274
+
275
+ const handleClose = useCallback(() => {
276
+ setIsOpen(false);
277
+ setSelectedImage(null);
278
+ }, []);
279
+
280
+ // Don't render if disabled or user is not admin/dev
281
+ if (!config.enabled || isLoading || !userRole) {
282
+ return null;
283
+ }
284
+
285
+ if (!isOpen || !selectedImage) {
286
+ return null;
287
+ }
288
+
289
+ return (
290
+ <div
291
+ className={`fixed inset-0 z-[9999] flex items-center justify-center ${config.overlayClassName || 'bg-black/50 backdrop-blur-sm'}`}
292
+ data-image-editor="true"
293
+ onClick={(e) => {
294
+ if (e.target === e.currentTarget) {
295
+ handleClose();
296
+ }
297
+ }}
298
+ >
299
+ <div
300
+ className={`relative bg-white dark:bg-neutral-900 rounded-2xl shadow-2xl max-w-7xl w-full mx-4 max-h-[90vh] flex flex-col ${config.className || ''}`}
301
+ onClick={(e) => e.stopPropagation()}
302
+ >
303
+ {/* Header */}
304
+ <div className="flex items-center justify-between p-6 border-b dark:border-neutral-800">
305
+ <h2 className="text-2xl font-bold dark:text-white">Edit Image</h2>
306
+ <button
307
+ onClick={handleClose}
308
+ className="p-2 hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-lg transition-colors"
309
+ aria-label="Close editor"
310
+ >
311
+ <X className="w-5 h-5" />
312
+ </button>
313
+ </div>
314
+
315
+ {/* Content */}
316
+ <div className="p-6">
317
+ <ImagePicker
318
+ value={selectedImage.element.getAttribute('data-image-id') || selectedImage.element.getAttribute('data-background-image-id') || selectedImage.originalSrc}
319
+ onChange={handleImageChangeWrapper}
320
+ brightness={selectedImage.brightness}
321
+ blur={selectedImage.blur}
322
+ scale={selectedImage.scale}
323
+ positionX={selectedImage.positionX}
324
+ positionY={selectedImage.positionY}
325
+ onBrightnessChange={handleBrightnessChangeWrapper}
326
+ onBlurChange={handleBlurChangeWrapper}
327
+ onEditorSave={handleEditorSave}
328
+ onScaleChange={async (newScale) => {
329
+ console.log('[GlobalImageEditor] onScaleChange called:', newScale);
330
+ if (selectedImage) {
331
+ const updated = { ...selectedImage, scale: newScale };
332
+ setSelectedImage(updated);
333
+ await saveImageTransformHandler(newScale, updated.positionX, updated.positionY, false);
334
+ }
335
+ }}
336
+ onPositionXChange={async (newPositionX) => {
337
+ console.log('[GlobalImageEditor] onPositionXChange called:', newPositionX);
338
+ if (selectedImage) {
339
+ const updated = { ...selectedImage, positionX: newPositionX };
340
+ setSelectedImage(updated);
341
+ await saveImageTransformHandler(updated.scale, newPositionX, updated.positionY, false);
342
+ }
343
+ }}
344
+ onPositionYChange={async (newPositionY) => {
345
+ console.log('[GlobalImageEditor] onPositionYChange called:', newPositionY);
346
+ if (selectedImage) {
347
+ const updated = { ...selectedImage, positionY: newPositionY };
348
+ setSelectedImage(updated);
349
+ // If onEditorSave wasn't provided, this might be called when editor closes
350
+ // In that case, we should save immediately with all current values including brightness/blur
351
+ // We'll save immediately to ensure the final state is persisted
352
+ await saveImageTransformHandler(
353
+ updated.scale,
354
+ updated.positionX,
355
+ newPositionY,
356
+ true, // Save immediately - this is likely the final callback when editor closes
357
+ updated.brightness,
358
+ updated.blur
359
+ );
360
+ }
361
+ }}
362
+ onEditorSave={handleEditorSave}
363
+ showEffects={true}
364
+ darkMode={false}
365
+ aspectRatio={selectedImage.aspectRatio}
366
+ borderRadius={selectedImage.borderRadius}
367
+ objectFit={selectedImage.objectFit}
368
+ objectPosition={selectedImage.objectPosition}
369
+ />
370
+ </div>
371
+ </div>
372
+ </div>
373
+ );
374
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Configuration utilities for GlobalImageEditor
3
+ */
4
+
5
+ import type { PluginConfig } from './types';
6
+
7
+ /**
8
+ * Read plugin configuration from window global
9
+ */
10
+ export function getPluginConfig(): PluginConfig {
11
+ if (typeof window === 'undefined') {
12
+ return { enabled: false };
13
+ }
14
+
15
+ const pluginProps = (window as any).__JHITS_PLUGIN_PROPS__?.['plugin-images'];
16
+ return {
17
+ enabled: pluginProps?.enabled !== undefined ? pluginProps.enabled : true,
18
+ className: pluginProps?.className,
19
+ overlayClassName: pluginProps?.overlayClassName,
20
+ };
21
+ }
@@ -0,0 +1,267 @@
1
+ /**
2
+ * Event handlers for image changes and effects
3
+ */
4
+
5
+ import type { SelectedImage } from './types';
6
+ import type { ImageMetadata } from '../../types';
7
+ import { getFilename, saveTransformToAPI, normalizePosition } from './saveLogic';
8
+
9
+ /**
10
+ * Apply image changes to DOM and save to API
11
+ */
12
+ export async function handleImageChange(
13
+ image: ImageMetadata | null,
14
+ selectedImage: SelectedImage,
15
+ onClose: () => void
16
+ ): Promise<void> {
17
+ if (!image) return;
18
+
19
+ const { element } = selectedImage;
20
+ const semanticId = element.getAttribute('data-image-id') ||
21
+ element.getAttribute('data-background-image-id');
22
+
23
+ if (!semanticId) {
24
+ console.error('[GlobalImageEditor] No semantic ID found for image');
25
+ return;
26
+ }
27
+
28
+ // Extract filename from image URL or use image.id
29
+ const filename = image.url.split('/').pop()?.split('?')[0] || image.id;
30
+ const normalizedPositionX = normalizePosition(selectedImage.positionX);
31
+ const normalizedPositionY = normalizePosition(selectedImage.positionY);
32
+
33
+ // Save the mapping between semantic ID and filename, including effects and transforms
34
+ await saveTransformToAPI(
35
+ semanticId,
36
+ filename,
37
+ selectedImage.scale,
38
+ normalizedPositionX,
39
+ normalizedPositionY,
40
+ selectedImage.brightness,
41
+ selectedImage.blur
42
+ );
43
+
44
+ // Update image src immediately - handle background images differently
45
+ if (selectedImage.isBackground) {
46
+ updateBackgroundImage(element, image.url, selectedImage, normalizedPositionX, normalizedPositionY);
47
+ } else {
48
+ updateRegularImage(element, image.url, semanticId, selectedImage, normalizedPositionX, normalizedPositionY);
49
+ }
50
+
51
+ // Dispatch a custom event to notify Image components to re-resolve
52
+ window.dispatchEvent(new CustomEvent('image-mapping-updated', {
53
+ detail: {
54
+ id: semanticId,
55
+ filename,
56
+ brightness: selectedImage.brightness,
57
+ blur: selectedImage.blur,
58
+ scale: selectedImage.scale,
59
+ positionX: selectedImage.positionX,
60
+ positionY: selectedImage.positionY,
61
+ }
62
+ }));
63
+
64
+ onClose();
65
+ }
66
+
67
+ /**
68
+ * Update background image element
69
+ */
70
+ function updateBackgroundImage(
71
+ element: HTMLElement,
72
+ imageUrl: string,
73
+ selectedImage: SelectedImage,
74
+ normalizedPositionX: number,
75
+ normalizedPositionY: number
76
+ ): void {
77
+ // For background images, update the nested Image component
78
+ const imgWrapper = element.querySelector('[data-image-id]');
79
+ if (imgWrapper) {
80
+ const img = imgWrapper.querySelector('img');
81
+ if (img) {
82
+ img.src = imageUrl;
83
+ img.setAttribute('data-edited-src', imageUrl);
84
+ img.style.transform = `scale(${selectedImage.scale}) translate(${normalizedPositionX}%, ${normalizedPositionY}%)`;
85
+ img.style.transformOrigin = 'center center';
86
+ }
87
+ } else {
88
+ // Fallback: update background-image style if no nested Image component
89
+ const bgImage = window.getComputedStyle(element).backgroundImage;
90
+ if (bgImage && bgImage !== 'none') {
91
+ element.style.backgroundImage = `url(${imageUrl})`;
92
+ }
93
+ }
94
+
95
+ // Store attributes on the background container
96
+ const semanticId = element.getAttribute('data-image-id') || element.getAttribute('data-background-image-id') || '';
97
+ element.setAttribute('data-background-image-id', semanticId);
98
+ element.setAttribute('data-edited-src', imageUrl);
99
+ element.setAttribute('data-brightness', selectedImage.brightness.toString());
100
+ element.setAttribute('data-blur', selectedImage.blur.toString());
101
+ element.setAttribute('data-scale', selectedImage.scale.toString());
102
+ element.setAttribute('data-position-x', normalizedPositionX.toString());
103
+ element.setAttribute('data-position-y', normalizedPositionY.toString());
104
+
105
+ // Apply filter to the wrapper element
106
+ if (selectedImage.brightness !== 100 || selectedImage.blur !== 0) {
107
+ element.style.filter = `brightness(${selectedImage.brightness}%) blur(${selectedImage.blur}px)`;
108
+ } else {
109
+ element.style.filter = '';
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Update regular image element
115
+ */
116
+ function updateRegularImage(
117
+ element: HTMLElement,
118
+ imageUrl: string,
119
+ semanticId: string,
120
+ selectedImage: SelectedImage,
121
+ normalizedPositionX: number,
122
+ normalizedPositionY: number
123
+ ): void {
124
+ if (element.tagName === 'IMG') {
125
+ (element as HTMLImageElement).src = imageUrl;
126
+ element.setAttribute('data-edited-src', imageUrl);
127
+ element.setAttribute('data-image-id', semanticId);
128
+ element.setAttribute('data-brightness', selectedImage.brightness.toString());
129
+ element.setAttribute('data-blur', selectedImage.blur.toString());
130
+
131
+ // Apply filter
132
+ if (selectedImage.brightness !== 100 || selectedImage.blur !== 0) {
133
+ element.style.filter = `brightness(${selectedImage.brightness}%) blur(${selectedImage.blur}px)`;
134
+ } else {
135
+ element.style.filter = '';
136
+ }
137
+ } else {
138
+ // Next.js Image wrapper - find the actual img element
139
+ const img = element.querySelector('img');
140
+ if (img) {
141
+ img.src = imageUrl;
142
+ img.setAttribute('data-edited-src', imageUrl);
143
+ img.style.transform = `scale(${selectedImage.scale}) translate(${normalizedPositionX}%, ${normalizedPositionY}%)`;
144
+ img.style.transformOrigin = 'center center';
145
+ }
146
+ element.setAttribute('data-image-id', semanticId);
147
+ element.setAttribute('data-edited-src', imageUrl);
148
+ element.setAttribute('data-brightness', selectedImage.brightness.toString());
149
+ element.setAttribute('data-blur', selectedImage.blur.toString());
150
+ element.setAttribute('data-scale', selectedImage.scale.toString());
151
+ element.setAttribute('data-position-x', normalizedPositionX.toString());
152
+ element.setAttribute('data-position-y', normalizedPositionY.toString());
153
+
154
+ // Apply filter to wrapper
155
+ if (selectedImage.brightness !== 100 || selectedImage.blur !== 0) {
156
+ element.style.filter = `brightness(${selectedImage.brightness}%) blur(${selectedImage.blur}px)`;
157
+ } else {
158
+ element.style.filter = '';
159
+ }
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Get filename from element for saving effects
165
+ */
166
+ async function getFilenameFromElement(
167
+ semanticId: string,
168
+ selectedImage: SelectedImage
169
+ ): Promise<string> {
170
+ const filename = await getFilename(semanticId, selectedImage);
171
+ if (filename) return filename;
172
+
173
+ const { element } = selectedImage;
174
+ if (selectedImage.isBackground) {
175
+ const bgImage = window.getComputedStyle(element).backgroundImage;
176
+ const urlMatch = bgImage.match(/url\(['"]?([^'"]+)['"]?\)/);
177
+ if (urlMatch && urlMatch[1]) {
178
+ return urlMatch[1].split('/api/uploads/')[1]?.split('?')[0] || semanticId;
179
+ }
180
+ } else {
181
+ let imgSrc: string | null = null;
182
+ if (element instanceof HTMLImageElement && element.src) {
183
+ imgSrc = element.src;
184
+ } else {
185
+ const img = element.querySelector('img') as HTMLImageElement | null;
186
+ if (img?.src) {
187
+ imgSrc = img.src;
188
+ }
189
+ }
190
+ if (imgSrc) {
191
+ return imgSrc.split('/api/uploads/')[1]?.split('?')[0] || semanticId;
192
+ }
193
+ }
194
+ return semanticId;
195
+ }
196
+
197
+ /**
198
+ * Handle brightness change
199
+ * @param saveImmediately - If true, save to API immediately. If false, only update state/DOM (for editor preview)
200
+ */
201
+ export async function handleBrightnessChange(
202
+ brightness: number,
203
+ selectedImage: SelectedImage,
204
+ setSelectedImage: (image: SelectedImage) => void,
205
+ saveImmediately: boolean = true
206
+ ): Promise<void> {
207
+ const { element } = selectedImage;
208
+ const semanticId = element.getAttribute('data-image-id') ||
209
+ element.getAttribute('data-background-image-id');
210
+
211
+ if (!semanticId) return;
212
+
213
+ const currentFilter = element.style.filter || '';
214
+ const blurMatch = currentFilter.match(/blur\((\d+)px\)/);
215
+ const blur = blurMatch ? parseInt(blurMatch[1]) : selectedImage.blur;
216
+
217
+ // Update the selectedImage state
218
+ setSelectedImage({
219
+ ...selectedImage,
220
+ brightness,
221
+ });
222
+
223
+ element.style.filter = `brightness(${brightness}%) blur(${blur}px)`;
224
+ element.setAttribute('data-brightness', brightness.toString());
225
+
226
+ // Save the effect immediately only if requested (not when in editor)
227
+ if (saveImmediately) {
228
+ const filename = await getFilenameFromElement(semanticId, selectedImage);
229
+ await saveTransformToAPI(semanticId, filename, selectedImage.scale, selectedImage.positionX, selectedImage.positionY, brightness, blur);
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Handle blur change
235
+ * @param saveImmediately - If true, save to API immediately. If false, only update state/DOM (for editor preview)
236
+ */
237
+ export async function handleBlurChange(
238
+ blur: number,
239
+ selectedImage: SelectedImage,
240
+ setSelectedImage: (image: SelectedImage) => void,
241
+ saveImmediately: boolean = true
242
+ ): Promise<void> {
243
+ const { element } = selectedImage;
244
+ const semanticId = element.getAttribute('data-image-id') ||
245
+ element.getAttribute('data-background-image-id');
246
+
247
+ if (!semanticId) return;
248
+
249
+ const currentFilter = element.style.filter || '';
250
+ const brightnessMatch = currentFilter.match(/brightness\((\d+)%\)/);
251
+ const brightness = brightnessMatch ? parseInt(brightnessMatch[1]) : selectedImage.brightness;
252
+
253
+ // Update the selectedImage state
254
+ setSelectedImage({
255
+ ...selectedImage,
256
+ blur,
257
+ });
258
+
259
+ element.style.filter = `brightness(${brightness}%) blur(${blur}px)`;
260
+ element.setAttribute('data-blur', blur.toString());
261
+
262
+ // Save the effect immediately only if requested (not when in editor)
263
+ if (saveImmediately) {
264
+ const filename = await getFilenameFromElement(semanticId, selectedImage);
265
+ await saveTransformToAPI(semanticId, filename, selectedImage.scale, selectedImage.positionX, selectedImage.positionY, brightness, blur);
266
+ }
267
+ }