@jhits/plugin-images 0.0.5 → 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 CHANGED
@@ -1,26 +1,26 @@
1
1
  {
2
2
  "name": "@jhits/plugin-images",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "description": "Image management and storage plugin for the JHITS ecosystem",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
8
- "main": "./src/index.tsx",
9
- "types": "./src/index.tsx",
8
+ "main": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
10
  "exports": {
11
11
  ".": {
12
- "types": "./src/index.tsx",
13
- "default": "./src/index.tsx"
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
14
  },
15
15
  "./server": {
16
- "types": "./src/index.server.ts",
17
- "default": "./src/index.server.ts"
16
+ "types": "./dist/index.server.d.ts",
17
+ "default": "./dist/index.server.js"
18
18
  }
19
19
  },
20
20
  "dependencies": {
21
- "mongodb": "^7.0.0",
22
- "lucide-react": "^0.562.0",
23
- "@jhits/plugin-core": "0.0.1"
21
+ "lucide-react": "^0.564.0",
22
+ "mongodb": "^7.1.0",
23
+ "@jhits/plugin-core": "0.0.3"
24
24
  },
25
25
  "peerDependencies": {
26
26
  "next": ">=15.0.0",
@@ -28,20 +28,24 @@
28
28
  "react-dom": ">=18.0.0"
29
29
  },
30
30
  "devDependencies": {
31
- "@tailwindcss/postcss": "^4",
32
- "@types/node": "^20.19.27",
33
- "@types/react": "^19",
34
- "@types/react-dom": "^19",
35
- "eslint": "^9",
36
- "eslint-config-next": "16.1.1",
37
- "next": "16.1.1",
38
- "react": "19.2.3",
39
- "react-dom": "19.2.3",
40
- "tailwindcss": "^4",
41
- "typescript": "^5"
31
+ "@tailwindcss/postcss": "^4.1.18",
32
+ "@types/node": "^25.2.3",
33
+ "@types/react": "^19.2.14",
34
+ "@types/react-dom": "^19.2.3",
35
+ "eslint": "^10.0.0",
36
+ "eslint-config-next": "16.1.6",
37
+ "next": "16.1.6",
38
+ "react": "19.2.4",
39
+ "react-dom": "19.2.4",
40
+ "tailwindcss": "^4.1.18",
41
+ "typescript": "^5.9.3"
42
42
  },
43
43
  "files": [
44
+ "dist",
44
45
  "src",
45
46
  "package.json"
46
- ]
47
+ ],
48
+ "scripts": {
49
+ "build": "tsc"
50
+ }
47
51
  }
@@ -236,10 +236,10 @@ export function GlobalImageEditor() {
236
236
  if (!isOpen || !selectedImage) {
237
237
  return null;
238
238
  }
239
-
239
+
240
240
  return (
241
241
  <div
242
- className={`fixed inset-0 z-[9999] flex items-center justify-center ${config.overlayClassName || 'bg-black/50 backdrop-blur-sm'}`}
242
+ className={`fixed inset-0 z-50 flex items-center justify-center ${config.overlayClassName || 'bg-black/50 backdrop-blur-sm'}`}
243
243
  data-image-editor="true"
244
244
  onClick={(e) => {
245
245
  if (e.target === e.currentTarget) {
@@ -289,10 +289,22 @@ export function ImageBrowserModal({
289
289
  const [resolvedSelectedId, setResolvedSelectedId] = useState<string | undefined>(selectedImageId);
290
290
  const [internalUploading, setInternalUploading] = useState(false);
291
291
  const [mounted, setMounted] = useState(false);
292
+ const [portalTarget, setPortalTarget] = useState<HTMLElement | null>(null);
292
293
 
293
- // Handle SSR - ensure we only render portal on client
294
+ // Handle SSR & portal target - ensure we only render portal on client
294
295
  useEffect(() => {
295
296
  setMounted(true);
297
+
298
+ if (typeof document === 'undefined') return;
299
+
300
+ // If we're inside the GlobalImageEditor overlay, portal into that instead of document.body
301
+ // This ensures the browser modal shares the same stacking context as the global editor
302
+ const editorContainer = document.querySelector('[data-image-editor="true"]') as HTMLElement | null;
303
+ if (editorContainer) {
304
+ setPortalTarget(editorContainer);
305
+ } else {
306
+ setPortalTarget(document.body);
307
+ }
296
308
  }, []);
297
309
 
298
310
  // Debug: Log when selectedImageId prop changes
@@ -650,10 +662,10 @@ export function ImageBrowserModal({
650
662
  onClose();
651
663
  };
652
664
 
653
- if (!isOpen || !mounted) return null;
665
+ if (!isOpen || !mounted || !portalTarget) return null;
654
666
 
655
667
  const modalContent = (
656
- <div className="fixed inset-0 z-[100] flex items-center justify-center p-4 animate-in fade-in duration-200">
668
+ <div className="fixed inset-0 z-[200] flex items-center justify-center p-4 animate-in fade-in duration-200">
657
669
  {/* Backdrop */}
658
670
  <div
659
671
  className="absolute inset-0 bg-neutral-950/70 backdrop-blur-md"
@@ -677,8 +689,9 @@ export function ImageBrowserModal({
677
689
  <button
678
690
  onClick={onClose}
679
691
  className="p-2 hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-xl transition-all duration-200 hover:scale-110 active:scale-95"
692
+ aria-label="Close"
680
693
  >
681
- <X size={20} className="text-neutral-400" />
694
+ <X size={20} className="text-neutral-500 dark:text-neutral-300 hover:text-neutral-700 dark:hover:text-white transition-colors" />
682
695
  </button>
683
696
  </div>
684
697
 
@@ -820,5 +833,5 @@ export function ImageBrowserModal({
820
833
  </div>
821
834
  );
822
835
 
823
- return createPortal(modalContent, document.body);
836
+ return createPortal(modalContent, portalTarget);
824
837
  }
@@ -9,6 +9,7 @@ import { ImageEditor, type ImageEditorHandle } from './ImageEditor';
9
9
  import { ImageBrowserModal } from './ImageBrowserModal';
10
10
  import { useImagePicker } from '../hooks/useImagePicker';
11
11
  import { getImageTransform, getImageFilter } from '../utils/transforms';
12
+ import { getFallbackImageUrl } from '../utils/fallback';
12
13
 
13
14
  export function ImagePicker({
14
15
  value, onChange, brightness = 100, blur = 0, scale = 1.0, positionX = 0, positionY = 0,
@@ -20,16 +21,34 @@ export function ImagePicker({
20
21
  const [isEditorOpen, setIsEditorOpen] = useState(false);
21
22
  const [isBrowserOpen, setIsBrowserOpen] = useState(false);
22
23
  const [previewBaseScale, setPreviewBaseScale] = useState<number | null>(null);
24
+ const [previewImageError, setPreviewImageError] = useState(false);
23
25
  const [mounted, setMounted] = useState(false);
26
+ const [portalTarget, setPortalTarget] = useState<HTMLElement | null>(null);
24
27
  const previewImageRef = useRef<HTMLImageElement>(null);
25
28
  const previewContainerRef = useRef<HTMLDivElement>(null);
26
29
  const editorRef = useRef<ImageEditorHandle | null>(null);
27
30
 
28
31
  const { selectedImage, setSelectedImage } = useImagePicker({ value, images: [] });
29
32
 
30
- // Handle SSR - ensure we only render portal on client
33
+ // Reset preview error when selected image changes
34
+ useEffect(() => {
35
+ setPreviewImageError(false);
36
+ }, [selectedImage?.id, selectedImage?.url]);
37
+
38
+ // Handle SSR & portal target - ensure we only render portal on client
31
39
  useEffect(() => {
32
40
  setMounted(true);
41
+
42
+ if (typeof document === 'undefined') return;
43
+
44
+ // If we're inside the GlobalImageEditor overlay, portal into that instead of document.body
45
+ // This ensures the browser/editor modals share the same stacking context
46
+ const editorContainer = previewContainerRef.current?.closest('[data-image-editor="true"]') as HTMLElement | null;
47
+ if (editorContainer) {
48
+ setPortalTarget(editorContainer);
49
+ } else {
50
+ setPortalTarget(document.body);
51
+ }
33
52
  }, []);
34
53
 
35
54
  // Auto-open editor if requested (e.g., when opening from edit button)
@@ -115,17 +134,36 @@ export function ImagePicker({
115
134
  <div className="relative group max-w-md mx-auto">
116
135
  <div className={`relative ${borderRadius} overflow-hidden border border-neutral-200 dark:border-neutral-800 bg-neutral-50 dark:bg-neutral-900/50 shadow-lg transition-all duration-300 hover:shadow-xl`} style={{ aspectRatio: aspectValue, width: '100%' }}>
117
136
  <div ref={previewContainerRef} className="relative w-full h-full overflow-hidden">
118
- <img
119
- ref={previewImageRef} src={selectedImage.url} alt={selectedImage.filename}
120
- className="absolute max-w-none" onLoad={calculatePreviewBaseScale}
121
- style={{
122
- top: '50%', left: '50%', width: 'auto', height: 'auto',
123
- minWidth: '100%', minHeight: '100%',
124
- filter: getImageFilter(brightness, blur),
125
- transform: previewBaseScale ? getImageTransform({ scale: transforms.scale, positionX: transforms.positionX, positionY: transforms.positionY, baseScale: previewBaseScale }, true) : 'translate(-50%, -50%)',
126
- transformOrigin: 'center center',
127
- }}
128
- />
137
+ {previewImageError ? (
138
+ <img
139
+ src={getFallbackImageUrl()}
140
+ alt={selectedImage.filename || 'Image not found'}
141
+ className="absolute max-w-none"
142
+ style={{
143
+ top: '50%', left: '50%', width: 'auto', height: 'auto',
144
+ minWidth: '100%', minHeight: '100%',
145
+ transform: 'translate(-50%, -50%)',
146
+ transformOrigin: 'center center',
147
+ objectFit: 'cover',
148
+ }}
149
+ />
150
+ ) : (
151
+ <img
152
+ ref={previewImageRef}
153
+ src={selectedImage.url}
154
+ alt={selectedImage.filename}
155
+ className="absolute max-w-none"
156
+ onLoad={calculatePreviewBaseScale}
157
+ onError={() => setPreviewImageError(true)}
158
+ style={{
159
+ top: '50%', left: '50%', width: 'auto', height: 'auto',
160
+ minWidth: '100%', minHeight: '100%',
161
+ filter: getImageFilter(brightness, blur),
162
+ transform: previewBaseScale ? getImageTransform({ scale: transforms.scale, positionX: transforms.positionX, positionY: transforms.positionY, baseScale: previewBaseScale }, true) : 'translate(-50%, -50%)',
163
+ transformOrigin: 'center center',
164
+ }}
165
+ />
166
+ )}
129
167
  </div>
130
168
  {/* Overlay on hover */}
131
169
  <div className="absolute inset-0 bg-black/0 group-hover:bg-black/5 transition-colors duration-300 pointer-events-none" />
@@ -161,8 +199,8 @@ export function ImagePicker({
161
199
  </button>
162
200
  </div>
163
201
 
164
- {isEditorOpen && selectedImage && mounted && createPortal(
165
- <div className="fixed inset-0 z-[100] flex items-center justify-center bg-neutral-950/80 dark:bg-neutral-950/90 backdrop-blur-md p-4 animate-in fade-in duration-200">
202
+ {isEditorOpen && selectedImage && mounted && portalTarget && createPortal(
203
+ <div className="fixed inset-0 z-[200] flex items-center justify-center bg-neutral-950/80 dark:bg-neutral-950/90 backdrop-blur-md p-4 animate-in fade-in duration-200">
166
204
  <div className="w-full max-w-4xl bg-white dark:bg-neutral-900 rounded-3xl overflow-hidden shadow-2xl dark:shadow-neutral-950/50 flex flex-col max-h-[85vh] border border-neutral-200 dark:border-neutral-800 animate-in zoom-in-95 duration-300">
167
205
  <div className="flex-1 overflow-hidden p-4 lg:p-6">
168
206
  <ImageEditor
@@ -201,7 +239,7 @@ export function ImagePicker({
201
239
  </div>
202
240
  </div>
203
241
  </div>,
204
- document.body
242
+ portalTarget
205
243
  )}
206
244
 
207
245
  <ImageBrowserModal
@@ -187,10 +187,21 @@ export function useImagePicker({ value, images }: UseImagePickerOptions) {
187
187
  lastResolvedValueRef.current = value;
188
188
  isResolvingRef.current = false;
189
189
  } else {
190
- // API resolution failed, create fallback object
190
+ // API resolution failed - check if it's a semantic ID or actual filename
191
+ const isLikelyFilename = /\.(jpg|jpeg|png|webp|gif|svg)$/i.test(imageIdToResolve) || /^\d+-/.test(imageIdToResolve);
192
+
193
+ if (!isLikelyFilename) {
194
+ // It's a semantic ID that couldn't be resolved - clear selection
195
+ // This prevents showing broken URLs like /api/uploads/blog-featured-...
196
+ setSelectedImage(null);
197
+ lastResolvedValueRef.current = value;
198
+ isResolvingRef.current = false;
199
+ return;
200
+ }
201
+
202
+ // It's a filename, create fallback object
191
203
  const isFullUrl = value.startsWith('http://') || value.startsWith('https://');
192
204
  const isRelativeUrl = value.startsWith('/');
193
- const isUrl = isFullUrl || isRelativeUrl;
194
205
 
195
206
  let imageUrl: string;
196
207
  if (isFullUrl) {
@@ -200,7 +211,7 @@ export function useImagePicker({ value, images }: UseImagePickerOptions) {
200
211
  // Already a relative URL starting with /, use as-is
201
212
  imageUrl = value;
202
213
  } else {
203
- // It's just a filename or ID, construct the URL
214
+ // It's a filename, construct the URL
204
215
  imageUrl = `/api/uploads/${value}`;
205
216
  }
206
217
 
@@ -225,10 +236,21 @@ export function useImagePicker({ value, images }: UseImagePickerOptions) {
225
236
  isResolvingRef.current = false;
226
237
  }
227
238
  } catch (error) {
228
- // API call failed, create fallback object
239
+ // API call failed - check if it's a semantic ID or actual filename
240
+ const isLikelyFilename = /\.(jpg|jpeg|png|webp|gif|svg)$/i.test(imageIdToResolve) || /^\d+-/.test(imageIdToResolve);
241
+
242
+ if (!isLikelyFilename) {
243
+ // It's a semantic ID that couldn't be resolved - clear selection
244
+ // This prevents showing broken URLs like /api/uploads/blog-featured-...
245
+ setSelectedImage(null);
246
+ lastResolvedValueRef.current = value;
247
+ isResolvingRef.current = false;
248
+ return;
249
+ }
250
+
251
+ // It's a filename, create fallback object
229
252
  const isFullUrl = value.startsWith('http://') || value.startsWith('https://');
230
253
  const isRelativeUrl = value.startsWith('/');
231
- const isUrl = isFullUrl || isRelativeUrl;
232
254
 
233
255
  let imageUrl: string;
234
256
  if (isFullUrl) {
@@ -238,7 +260,7 @@ export function useImagePicker({ value, images }: UseImagePickerOptions) {
238
260
  // Already a relative URL starting with /, use as-is
239
261
  imageUrl = value;
240
262
  } else {
241
- // It's just a filename or ID, construct the URL
263
+ // It's a filename, construct the URL
242
264
  imageUrl = `/api/uploads/${value}`;
243
265
  }
244
266
 
@@ -1,374 +0,0 @@
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
- }