@jhits/plugin-images 0.0.8 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/package.json +2 -3
  2. package/src/api/fallback/route.ts +0 -69
  3. package/src/api/index.ts +0 -10
  4. package/src/api/list/index.ts +0 -96
  5. package/src/api/resolve/route.ts +0 -241
  6. package/src/api/router.ts +0 -85
  7. package/src/api/upload/index.ts +0 -88
  8. package/src/api/uploads/[filename]/route.ts +0 -93
  9. package/src/api-server.ts +0 -11
  10. package/src/assets/noimagefound.jpg +0 -0
  11. package/src/components/BackgroundImage.d.ts +0 -11
  12. package/src/components/BackgroundImage.d.ts.map +0 -1
  13. package/src/components/BackgroundImage.js +0 -35
  14. package/src/components/BackgroundImage.tsx +0 -92
  15. package/src/components/GlobalImageEditor/config.d.ts +0 -9
  16. package/src/components/GlobalImageEditor/config.d.ts.map +0 -1
  17. package/src/components/GlobalImageEditor/config.js +0 -18
  18. package/src/components/GlobalImageEditor/config.ts +0 -21
  19. package/src/components/GlobalImageEditor/eventHandlers.d.ts +0 -20
  20. package/src/components/GlobalImageEditor/eventHandlers.d.ts.map +0 -1
  21. package/src/components/GlobalImageEditor/eventHandlers.js +0 -206
  22. package/src/components/GlobalImageEditor/eventHandlers.ts +0 -267
  23. package/src/components/GlobalImageEditor/imageDetection.d.ts +0 -16
  24. package/src/components/GlobalImageEditor/imageDetection.d.ts.map +0 -1
  25. package/src/components/GlobalImageEditor/imageDetection.js +0 -130
  26. package/src/components/GlobalImageEditor/imageDetection.ts +0 -160
  27. package/src/components/GlobalImageEditor/imageSetup.d.ts +0 -9
  28. package/src/components/GlobalImageEditor/imageSetup.d.ts.map +0 -1
  29. package/src/components/GlobalImageEditor/imageSetup.js +0 -261
  30. package/src/components/GlobalImageEditor/imageSetup.ts +0 -306
  31. package/src/components/GlobalImageEditor/saveLogic.d.ts +0 -26
  32. package/src/components/GlobalImageEditor/saveLogic.d.ts.map +0 -1
  33. package/src/components/GlobalImageEditor/saveLogic.js +0 -99
  34. package/src/components/GlobalImageEditor/saveLogic.ts +0 -133
  35. package/src/components/GlobalImageEditor/stylingDetection.d.ts +0 -9
  36. package/src/components/GlobalImageEditor/stylingDetection.d.ts.map +0 -1
  37. package/src/components/GlobalImageEditor/stylingDetection.js +0 -110
  38. package/src/components/GlobalImageEditor/stylingDetection.ts +0 -122
  39. package/src/components/GlobalImageEditor/transformParsing.d.ts +0 -16
  40. package/src/components/GlobalImageEditor/transformParsing.d.ts.map +0 -1
  41. package/src/components/GlobalImageEditor/transformParsing.js +0 -68
  42. package/src/components/GlobalImageEditor/transformParsing.ts +0 -83
  43. package/src/components/GlobalImageEditor/types.d.ts +0 -36
  44. package/src/components/GlobalImageEditor/types.d.ts.map +0 -1
  45. package/src/components/GlobalImageEditor/types.js +0 -4
  46. package/src/components/GlobalImageEditor/types.ts +0 -39
  47. package/src/components/GlobalImageEditor.d.ts +0 -8
  48. package/src/components/GlobalImageEditor.d.ts.map +0 -1
  49. package/src/components/GlobalImageEditor.js +0 -227
  50. package/src/components/GlobalImageEditor.tsx +0 -327
  51. package/src/components/Image.d.ts +0 -22
  52. package/src/components/Image.d.ts.map +0 -1
  53. package/src/components/Image.js +0 -229
  54. package/src/components/Image.tsx +0 -343
  55. package/src/components/ImageBrowserModal.d.ts +0 -13
  56. package/src/components/ImageBrowserModal.d.ts.map +0 -1
  57. package/src/components/ImageBrowserModal.js +0 -504
  58. package/src/components/ImageBrowserModal.tsx +0 -837
  59. package/src/components/ImageEditor.d.ts +0 -27
  60. package/src/components/ImageEditor.d.ts.map +0 -1
  61. package/src/components/ImageEditor.js +0 -173
  62. package/src/components/ImageEditor.tsx +0 -323
  63. package/src/components/ImageEffectsPanel.tsx +0 -116
  64. package/src/components/ImagePicker.d.ts +0 -3
  65. package/src/components/ImagePicker.d.ts.map +0 -1
  66. package/src/components/ImagePicker.js +0 -143
  67. package/src/components/ImagePicker.tsx +0 -265
  68. package/src/components/ImagesPluginInit.d.ts +0 -24
  69. package/src/components/ImagesPluginInit.d.ts.map +0 -1
  70. package/src/components/ImagesPluginInit.js +0 -28
  71. package/src/components/ImagesPluginInit.tsx +0 -31
  72. package/src/components/index.ts +0 -10
  73. package/src/config.ts +0 -179
  74. package/src/hooks/useImagePicker.d.ts +0 -20
  75. package/src/hooks/useImagePicker.d.ts.map +0 -1
  76. package/src/hooks/useImagePicker.js +0 -322
  77. package/src/hooks/useImagePicker.ts +0 -344
  78. package/src/index.d.ts +0 -23
  79. package/src/index.d.ts.map +0 -1
  80. package/src/index.js +0 -28
  81. package/src/index.server.ts +0 -12
  82. package/src/index.tsx +0 -56
  83. package/src/init.d.ts +0 -33
  84. package/src/init.d.ts.map +0 -1
  85. package/src/init.js +0 -43
  86. package/src/init.tsx +0 -58
  87. package/src/types/index.d.ts +0 -80
  88. package/src/types/index.d.ts.map +0 -1
  89. package/src/types/index.js +0 -4
  90. package/src/types/index.ts +0 -84
  91. package/src/utils/fallback.d.ts +0 -27
  92. package/src/utils/fallback.d.ts.map +0 -1
  93. package/src/utils/fallback.js +0 -63
  94. package/src/utils/fallback.ts +0 -73
  95. package/src/utils/transforms.d.ts +0 -26
  96. package/src/utils/transforms.d.ts.map +0 -1
  97. package/src/utils/transforms.js +0 -38
  98. package/src/utils/transforms.ts +0 -54
  99. package/src/views/ImageManager.d.ts +0 -10
  100. package/src/views/ImageManager.d.ts.map +0 -1
  101. package/src/views/ImageManager.js +0 -9
  102. package/src/views/ImageManager.tsx +0 -30
@@ -1,504 +0,0 @@
1
- 'use client';
2
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
4
- import { createPortal } from 'react-dom';
5
- import { Upload, Search, X, Image as ImageIcon, Check, Link as LinkIcon, Loader2, Grid3x3 } from 'lucide-react';
6
- // Masonry Grid Component that maintains item positions
7
- function MasonryGrid({ images, selectedImageId, onSelectImage, onClose, fixedColumns, }) {
8
- const containerRef = useRef(null);
9
- const [columnCount, setColumnCount] = useState(fixedColumns || 2);
10
- // Calculate column count based on screen size (only if not fixed)
11
- useEffect(() => {
12
- if (fixedColumns) {
13
- setColumnCount(fixedColumns);
14
- return;
15
- }
16
- const updateColumnCount = () => {
17
- const width = window.innerWidth;
18
- let cols = 2;
19
- if (width >= 1280)
20
- cols = 6;
21
- else if (width >= 1024)
22
- cols = 5;
23
- else if (width >= 768)
24
- cols = 4;
25
- else if (width >= 640)
26
- cols = 3;
27
- setColumnCount(cols);
28
- };
29
- updateColumnCount();
30
- window.addEventListener('resize', updateColumnCount);
31
- return () => window.removeEventListener('resize', updateColumnCount);
32
- }, [fixedColumns]);
33
- // Distribute images into columns using stable round-robin assignment
34
- // This ensures items NEVER move once rendered - they always go to the same column
35
- // based on their index, preventing reshuffling when new items are added
36
- const columns = useMemo(() => {
37
- const cols = Array.from({ length: columnCount }, () => []);
38
- images.forEach((image, index) => {
39
- // Round-robin: image at index N always goes to column (N % columnCount)
40
- // This guarantees stable positions - items never move columns
41
- const columnIndex = index % columnCount;
42
- cols[columnIndex].push(image);
43
- });
44
- return cols;
45
- }, [images, columnCount]);
46
- return (_jsx("div", { ref: containerRef, className: "grid gap-4", style: {
47
- gridTemplateColumns: `repeat(${columnCount}, minmax(0, 1fr))`,
48
- }, children: columns.map((columnImages, columnIndex) => (_jsx("div", { className: "flex flex-col gap-4", children: columnImages.map((image, imageIndex) => {
49
- var _a;
50
- // Normalize both selectedImageId and image identifiers for matching
51
- let normalizedSelectedId = selectedImageId;
52
- let normalizedImageUrl = image.url;
53
- let normalizedImageId = image.id;
54
- let normalizedImageFilename = image.filename;
55
- // Extract filename from selectedImageId if it's a URL
56
- if (selectedImageId) {
57
- if (selectedImageId.includes('/')) {
58
- const urlParts = selectedImageId.split('/');
59
- normalizedSelectedId = ((_a = urlParts[urlParts.length - 1]) === null || _a === void 0 ? void 0 : _a.split('?')[0]) || selectedImageId;
60
- }
61
- }
62
- // Normalize image URL (remove protocol, domain, query params)
63
- if (image.url) {
64
- // Remove protocol and domain if present
65
- normalizedImageUrl = image.url.replace(/^https?:\/\/[^\/]+/, '');
66
- // Remove query params
67
- normalizedImageUrl = normalizedImageUrl.split('?')[0];
68
- // Extract just the filename
69
- const urlParts = normalizedImageUrl.split('/');
70
- normalizedImageUrl = urlParts[urlParts.length - 1] || normalizedImageUrl;
71
- }
72
- // Check if image is selected by comparing all possible combinations
73
- const isSelected = Boolean(selectedImageId && (
74
- // Direct matches
75
- selectedImageId === image.id ||
76
- selectedImageId === image.url ||
77
- selectedImageId === image.filename ||
78
- // Normalized filename matches
79
- normalizedSelectedId === image.id ||
80
- normalizedSelectedId === image.filename ||
81
- normalizedSelectedId === normalizedImageUrl ||
82
- normalizedSelectedId === normalizedImageId ||
83
- normalizedSelectedId === normalizedImageFilename ||
84
- // URL contains/ends with matches
85
- (image.url && selectedImageId && (image.url.includes(selectedImageId) ||
86
- image.url.endsWith(selectedImageId) ||
87
- selectedImageId.includes(image.url) ||
88
- selectedImageId.endsWith(image.url) ||
89
- (normalizedSelectedId && (image.url.includes(normalizedSelectedId) ||
90
- image.url.endsWith(normalizedSelectedId))))) ||
91
- // Filename matches
92
- (image.filename && (image.filename === normalizedSelectedId ||
93
- normalizedSelectedId === image.filename)) ||
94
- // ID matches
95
- (image.id && (image.id === normalizedSelectedId ||
96
- normalizedSelectedId === image.id))));
97
- // Use a unique key combining column index, image index, and image id
98
- // This ensures uniqueness even if image.id is duplicated
99
- const uniqueKey = `${columnIndex}-${imageIndex}-${image.id}-${image.url}`;
100
- return (_jsx(ImageCard, { image: image, isSelected: isSelected, onSelect: () => {
101
- onSelectImage(image);
102
- onClose();
103
- } }, uniqueKey));
104
- }) }, columnIndex))) }));
105
- }
106
- // Image Card Component for Masonry Layout
107
- function ImageCard({ image, isSelected, onSelect }) {
108
- const [imageLoaded, setImageLoaded] = useState(false);
109
- const [imageError, setImageError] = useState(false);
110
- const imgRef = useRef(null);
111
- useEffect(() => {
112
- // Check if image is already loaded (cached)
113
- const img = imgRef.current;
114
- if (img) {
115
- if (img.complete && img.naturalWidth > 0) {
116
- setImageLoaded(true);
117
- }
118
- else {
119
- // Fallback: if image doesn't load within 5 seconds, show it anyway
120
- const timeout = setTimeout(() => {
121
- setImageLoaded(true);
122
- }, 5000);
123
- return () => clearTimeout(timeout);
124
- }
125
- }
126
- }, [image.url]);
127
- const handleLoad = () => {
128
- setImageLoaded(true);
129
- setImageError(false);
130
- };
131
- const handleError = (e) => {
132
- console.error('[ImageCard] Image failed to load:', image.url, e);
133
- setImageError(true);
134
- setImageLoaded(false);
135
- };
136
- return (_jsxs("button", { onClick: onSelect, className: `group relative w-full rounded-xl overflow-hidden bg-neutral-100 dark:bg-neutral-800 transition-all duration-200 active:scale-[0.98] shadow-sm hover:shadow-lg block ${isSelected ? 'ring-2 ring-primary ring-offset-1 ring-offset-white dark:ring-offset-neutral-900' : ''}`, children: [!imageError ? (_jsxs("div", { className: "relative w-full group/image", children: [_jsx("img", { ref: imgRef, src: image.url, alt: image.filename, onLoad: handleLoad, onError: handleError, className: `w-full h-auto object-cover transition-all duration-300 group-hover/image:scale-105 block ${!imageLoaded ? 'opacity-0' : 'opacity-100'}`, loading: "lazy" }), !imageLoaded && (_jsx("div", { className: "absolute inset-0 flex items-center justify-center bg-neutral-200 dark:bg-neutral-800 z-10", children: _jsx(Loader2, { size: 20, className: "text-neutral-400 animate-spin" }) }))] })) : (_jsxs("div", { className: "w-full aspect-square flex flex-col items-center justify-center bg-neutral-200 dark:bg-neutral-800 text-neutral-400", children: [_jsx(ImageIcon, { size: 24, className: "mb-2" }), _jsx("p", { className: "text-xs font-medium", children: "Failed to load" })] })), isSelected && (_jsxs(_Fragment, { children: [_jsx("div", { className: "absolute inset-0 border-2 border-primary rounded-xl pointer-events-none z-30 ring-2 ring-primary/20" }), _jsxs("div", { className: "absolute top-2 right-2 bg-primary text-white rounded-md px-2 py-1 shadow-md z-30 flex items-center gap-1.5 backdrop-blur-sm", children: [_jsx(Check, { size: 13, strokeWidth: 2.5, className: "flex-shrink-0" }), _jsx("span", { className: "text-[10px] font-semibold uppercase tracking-wide", children: "Selected" })] }), _jsx("div", { className: "absolute inset-0 bg-primary/8 dark:bg-primary/12 rounded-xl pointer-events-none z-20" })] })), _jsx("div", { className: "absolute inset-0 bg-black/0 group-hover:bg-black/10 transition-colors duration-200 rounded-xl pointer-events-none" }), _jsx("div", { className: "absolute bottom-0 left-0 right-0 p-2 bg-gradient-to-t from-black/70 via-black/50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-200 rounded-b-xl pointer-events-none", children: _jsx("p", { className: "text-[10px] text-white font-semibold truncate leading-tight", children: image.filename }) })] }));
137
- }
138
- const ITEMS_PER_PAGE = 100; // Increased from 24 to load more images per request
139
- export function ImageBrowserModal({ isOpen, onClose, onSelectImage, selectedImageId, darkMode = false, onUpload, uploading = false, }) {
140
- const [searchQuery, setSearchQuery] = useState('');
141
- const [externalUrl, setExternalUrl] = useState('');
142
- const [images, setImages] = useState([]);
143
- const [loading, setLoading] = useState(false);
144
- const [loadingMore, setLoadingMore] = useState(false);
145
- const [page, setPage] = useState(1);
146
- const [hasMore, setHasMore] = useState(true);
147
- const [total, setTotal] = useState(0);
148
- const [resolvedSelectedId, setResolvedSelectedId] = useState(selectedImageId);
149
- const [internalUploading, setInternalUploading] = useState(false);
150
- const [mounted, setMounted] = useState(false);
151
- const [portalTarget, setPortalTarget] = useState(null);
152
- // Handle SSR & portal target - ensure we only render portal on client
153
- useEffect(() => {
154
- setMounted(true);
155
- if (typeof document === 'undefined')
156
- return;
157
- // If we're inside the GlobalImageEditor overlay, portal into that instead of document.body
158
- // This ensures the browser modal shares the same stacking context as the global editor
159
- const editorContainer = document.querySelector('[data-image-editor="true"]');
160
- if (editorContainer) {
161
- setPortalTarget(editorContainer);
162
- }
163
- else {
164
- setPortalTarget(document.body);
165
- }
166
- }, []);
167
- // Debug: Log when selectedImageId prop changes
168
- useEffect(() => {
169
- setResolvedSelectedId(selectedImageId);
170
- }, [selectedImageId]);
171
- // Load column count from localStorage, default to 3
172
- const getStoredColumnCount = () => {
173
- if (typeof window === 'undefined')
174
- return 3;
175
- try {
176
- const stored = localStorage.getItem('plugin-images-column-count');
177
- if (stored) {
178
- const parsed = parseInt(stored, 10);
179
- // Validate: must be between 3 and 9
180
- if (!isNaN(parsed) && parsed >= 3 && parsed <= 9) {
181
- return parsed;
182
- }
183
- }
184
- }
185
- catch (error) {
186
- // Silently fail - use default
187
- }
188
- return 3;
189
- };
190
- const [columnCount, setColumnCount] = useState(getStoredColumnCount);
191
- // Save column count to localStorage whenever it changes
192
- useEffect(() => {
193
- if (typeof window === 'undefined')
194
- return;
195
- try {
196
- localStorage.setItem('plugin-images-column-count', columnCount.toString());
197
- }
198
- catch (error) {
199
- // Silently fail
200
- }
201
- }, [columnCount]);
202
- const modalRef = useRef(null);
203
- const scrollContainerRef = useRef(null);
204
- const fileInputRef = useRef(null);
205
- const searchTimeoutRef = useRef(null);
206
- // Load images from API with pagination
207
- const loadImages = useCallback(async (pageNum = 1, append = false) => {
208
- if (append) {
209
- setLoadingMore(true);
210
- }
211
- else {
212
- setLoading(true);
213
- }
214
- try {
215
- const searchParam = searchQuery.trim() ? `&search=${encodeURIComponent(searchQuery.trim())}` : '';
216
- const response = await fetch(`/api/plugin-images/list?page=${pageNum}&limit=${ITEMS_PER_PAGE}${searchParam}`);
217
- if (response.ok) {
218
- const data = await response.json();
219
- const newImages = (data.images || []).map((img) => {
220
- const imageUrl = img.url || `/api/uploads/${img.filename}`;
221
- // Ensure URL is absolute if it's a relative path
222
- const finalUrl = imageUrl.startsWith('http') ? imageUrl : imageUrl.startsWith('/') ? imageUrl : `/${imageUrl}`;
223
- return Object.assign(Object.assign({}, img), { url: finalUrl });
224
- });
225
- setTotal(data.total || 0);
226
- if (append) {
227
- setImages(prev => {
228
- // Deduplicate: use a Map to track unique images by id
229
- const imageMap = new Map();
230
- // Add existing images
231
- prev.forEach((img) => {
232
- imageMap.set(img.id, img);
233
- });
234
- // Add new images (will overwrite duplicates)
235
- newImages.forEach((img) => {
236
- imageMap.set(img.id, img);
237
- });
238
- const updated = Array.from(imageMap.values());
239
- // More images available if: we got a full page AND haven't loaded all images
240
- setHasMore(newImages.length === ITEMS_PER_PAGE && updated.length < (data.total || 0));
241
- return updated;
242
- });
243
- }
244
- else {
245
- // Deduplicate even for initial load
246
- const imageMap = new Map();
247
- newImages.forEach((img) => {
248
- imageMap.set(img.id, img);
249
- });
250
- const deduplicated = Array.from(imageMap.values());
251
- setImages(deduplicated);
252
- // More images available if: we got a full page AND haven't loaded all images
253
- setHasMore(deduplicated.length === ITEMS_PER_PAGE && deduplicated.length < (data.total || 0));
254
- }
255
- setPage(pageNum);
256
- }
257
- else {
258
- console.error('[ImageBrowserModal] Failed to load images:', response.status);
259
- }
260
- }
261
- catch (error) {
262
- console.error('[ImageBrowserModal] Failed to load images:', error);
263
- }
264
- finally {
265
- setLoading(false);
266
- setLoadingMore(false);
267
- }
268
- }, [searchQuery]);
269
- // Load multiple pages in parallel for faster initial load
270
- const loadInitialImages = useCallback(async () => {
271
- setLoading(true);
272
- try {
273
- const searchParam = searchQuery.trim() ? `&search=${encodeURIComponent(searchQuery.trim())}` : '';
274
- // Load first 2-3 pages in parallel to fill the screen quickly
275
- const pagesToLoad = 3;
276
- const requests = Array.from({ length: pagesToLoad }, (_, i) => fetch(`/api/plugin-images/list?page=${i + 1}&limit=${ITEMS_PER_PAGE}${searchParam}`));
277
- const responses = await Promise.all(requests);
278
- const results = await Promise.all(responses.map(r => r.ok ? r.json() : { images: [], total: 0 }));
279
- // Combine all images
280
- const allImages = [];
281
- let totalCount = 0;
282
- results.forEach((data, index) => {
283
- if (data.images && data.images.length > 0) {
284
- const pageImages = data.images.map((img) => {
285
- const imageUrl = img.url || `/api/uploads/${img.filename}`;
286
- const finalUrl = imageUrl.startsWith('http') ? imageUrl : imageUrl.startsWith('/') ? imageUrl : `/${imageUrl}`;
287
- return Object.assign(Object.assign({}, img), { url: finalUrl });
288
- });
289
- allImages.push(...pageImages);
290
- }
291
- if (index === 0 && data.total) {
292
- totalCount = data.total;
293
- }
294
- });
295
- // Deduplicate
296
- const imageMap = new Map();
297
- allImages.forEach((img) => {
298
- imageMap.set(img.id, img);
299
- });
300
- const deduplicated = Array.from(imageMap.values());
301
- setImages(deduplicated);
302
- setTotal(totalCount);
303
- setHasMore(deduplicated.length < totalCount);
304
- setPage(pagesToLoad);
305
- }
306
- catch (error) {
307
- console.error('[ImageBrowserModal] Error loading initial images:', error);
308
- // Fallback to single page load
309
- loadImages(1, false);
310
- }
311
- finally {
312
- setLoading(false);
313
- }
314
- }, [searchQuery, loadImages]);
315
- // Load more images when scrolling near bottom
316
- const handleScroll = useCallback(() => {
317
- const container = scrollContainerRef.current;
318
- if (!container || loadingMore || !hasMore || loading)
319
- return;
320
- const scrollTop = container.scrollTop;
321
- const scrollHeight = container.scrollHeight;
322
- const clientHeight = container.clientHeight;
323
- // Calculate distance from bottom
324
- const distanceFromBottom = scrollHeight - (scrollTop + clientHeight);
325
- // Load more when within 200px of bottom
326
- if (distanceFromBottom < 200) {
327
- loadImages(page + 1, true);
328
- }
329
- }, [page, hasMore, loadingMore, loading, loadImages]);
330
- // Check if viewport needs more images to fill the screen
331
- const checkAndLoadMoreIfNeeded = useCallback(() => {
332
- const container = scrollContainerRef.current;
333
- if (!container || loading || loadingMore || !hasMore)
334
- return;
335
- // Check if content fills the viewport (with some margin)
336
- const scrollHeight = container.scrollHeight;
337
- const clientHeight = container.clientHeight;
338
- const needsMoreContent = scrollHeight <= clientHeight + 100; // 100px margin
339
- if (needsMoreContent) {
340
- loadImages(page + 1, true);
341
- }
342
- }, [loading, loadingMore, hasMore, images.length, page, loadImages]);
343
- // Auto-load more images when images change and viewport isn't filled
344
- useEffect(() => {
345
- if (!isOpen || images.length === 0 || loading || loadingMore)
346
- return;
347
- // Wait for DOM to update after images are set and rendered
348
- const timeoutId = setTimeout(() => {
349
- checkAndLoadMoreIfNeeded();
350
- }, 200); // Delay to allow images to render and layout to settle
351
- return () => clearTimeout(timeoutId);
352
- }, [images, isOpen, loading, loadingMore, checkAndLoadMoreIfNeeded]);
353
- // Check again after loading completes
354
- useEffect(() => {
355
- if (!isOpen || images.length === 0)
356
- return;
357
- if (!loading && !loadingMore) {
358
- // Loading just finished, check if we need more
359
- const timeoutId = setTimeout(() => {
360
- checkAndLoadMoreIfNeeded();
361
- }, 300); // Give images time to render
362
- return () => clearTimeout(timeoutId);
363
- }
364
- }, [loading, loadingMore, isOpen, images.length, checkAndLoadMoreIfNeeded]);
365
- // Also check when column count changes (more columns = need more images)
366
- useEffect(() => {
367
- if (!isOpen || images.length === 0 || loading || loadingMore)
368
- return;
369
- const timeoutId = setTimeout(() => {
370
- checkAndLoadMoreIfNeeded();
371
- }, 200);
372
- return () => clearTimeout(timeoutId);
373
- }, [columnCount, isOpen, loading, loadingMore, checkAndLoadMoreIfNeeded]);
374
- // Initial load and reset on open
375
- useEffect(() => {
376
- if (isOpen) {
377
- document.body.style.overflow = 'hidden';
378
- setPage(1);
379
- setImages([]);
380
- setHasMore(true);
381
- // Use parallel loading for faster initial load
382
- loadInitialImages();
383
- }
384
- else {
385
- document.body.style.overflow = 'unset';
386
- setSearchQuery('');
387
- setImages([]);
388
- setPage(1);
389
- }
390
- return () => { document.body.style.overflow = 'unset'; };
391
- }, [isOpen, loadInitialImages, selectedImageId]);
392
- // Debounced search
393
- useEffect(() => {
394
- if (!isOpen)
395
- return;
396
- if (searchTimeoutRef.current) {
397
- clearTimeout(searchTimeoutRef.current);
398
- }
399
- searchTimeoutRef.current = setTimeout(() => {
400
- setPage(1);
401
- setImages([]);
402
- setHasMore(true);
403
- // For search, use parallel loading if no search query (faster), otherwise single page
404
- if (!searchQuery.trim()) {
405
- loadInitialImages();
406
- }
407
- else {
408
- loadImages(1, false);
409
- }
410
- }, 300);
411
- return () => {
412
- if (searchTimeoutRef.current) {
413
- clearTimeout(searchTimeoutRef.current);
414
- }
415
- };
416
- }, [searchQuery, isOpen, loadImages, loadInitialImages]);
417
- // Reload after upload
418
- useEffect(() => {
419
- if (!uploading && isOpen && images.length > 0) {
420
- // Reload first page to show newly uploaded image
421
- loadImages(1, false);
422
- }
423
- }, [uploading, isOpen, loadImages]);
424
- // Attach scroll listener
425
- useEffect(() => {
426
- const container = scrollContainerRef.current;
427
- if (!container)
428
- return;
429
- container.addEventListener('scroll', handleScroll);
430
- return () => container.removeEventListener('scroll', handleScroll);
431
- }, [handleScroll]);
432
- // Handle file upload (default handler if onUpload prop is not provided)
433
- const handleFileUpload = async (file) => {
434
- // If onUpload prop is provided, use it
435
- if (onUpload) {
436
- await onUpload(file);
437
- return;
438
- }
439
- // Otherwise, handle upload internally
440
- setInternalUploading(true);
441
- try {
442
- const formData = new FormData();
443
- formData.append('file', file);
444
- const response = await fetch('/api/plugin-images/upload', {
445
- method: 'POST',
446
- body: formData,
447
- });
448
- const data = await response.json();
449
- if (data.success && data.image) {
450
- // Reload images to show the newly uploaded image
451
- await loadImages(1, false);
452
- // Optionally select the newly uploaded image
453
- // onSelectImage(data.image);
454
- }
455
- else {
456
- console.error('[ImageBrowserModal] Upload failed:', data.error || 'Unknown error');
457
- alert(data.error || 'Failed to upload image');
458
- }
459
- }
460
- catch (error) {
461
- console.error('[ImageBrowserModal] Upload error:', error);
462
- alert('Failed to upload image');
463
- }
464
- finally {
465
- setInternalUploading(false);
466
- // Reset file input
467
- if (fileInputRef.current) {
468
- fileInputRef.current.value = '';
469
- }
470
- }
471
- };
472
- // 4. Handle External URL Logic
473
- const handleUseExternalUrl = () => {
474
- var _a;
475
- const url = externalUrl.trim();
476
- if (!url)
477
- return;
478
- const externalImage = {
479
- id: `external-${Date.now()}`,
480
- filename: ((_a = url.split('/').pop()) === null || _a === void 0 ? void 0 : _a.split('?')[0]) || 'external-image',
481
- url: url,
482
- size: 0,
483
- mimeType: 'image/external',
484
- uploadedAt: new Date().toISOString(),
485
- };
486
- onSelectImage(externalImage);
487
- setExternalUrl('');
488
- onClose();
489
- };
490
- if (!isOpen || !mounted || !portalTarget)
491
- return null;
492
- const modalContent = (_jsxs("div", { className: "fixed inset-0 z-[200] flex items-center justify-center p-4 animate-in fade-in duration-200", children: [_jsx("div", { className: "absolute inset-0 bg-neutral-950/70 backdrop-blur-md", onClick: onClose }), _jsxs("div", { ref: modalRef, className: `relative w-full max-w-6xl h-[90vh] bg-white dark:bg-neutral-900 rounded-3xl shadow-2xl flex flex-col overflow-hidden border border-neutral-200 dark:border-neutral-800 animate-in zoom-in-95 duration-300 ${darkMode ? 'dark' : ''}`, children: [_jsxs("div", { className: "px-6 py-5 border-b border-neutral-200 dark:border-neutral-800 bg-gradient-to-r from-neutral-50 to-white dark:from-neutral-900 dark:to-neutral-950 flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("h2", { className: "text-2xl font-black uppercase tracking-tight dark:text-white", children: "Media Library" }), total > 0 && (_jsxs("p", { className: "text-xs text-neutral-500 font-semibold uppercase tracking-wider mt-1", children: [total, " ", total === 1 ? 'image' : 'images'] }))] }), _jsx("button", { onClick: onClose, className: "p-2 hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-xl transition-all duration-200 hover:scale-110 active:scale-95", "aria-label": "Close", children: _jsx(X, { size: 20, className: "text-neutral-500 dark:text-neutral-300 hover:text-neutral-700 dark:hover:text-white transition-colors" }) })] }), _jsxs("div", { className: "px-6 py-4 bg-neutral-50/50 dark:bg-neutral-900/30 border-b border-neutral-200 dark:border-neutral-800 flex flex-col sm:flex-row gap-3", children: [_jsxs("div", { className: "relative flex-1", children: [_jsx(Search, { size: 16, className: "absolute left-3.5 top-1/2 -translate-y-1/2 text-neutral-400" }), _jsx("input", { type: "text", placeholder: "Search images...", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), className: "w-full pl-10 pr-4 py-2.5 bg-white dark:bg-neutral-950 border border-neutral-200 dark:border-neutral-800 rounded-xl text-sm font-medium focus:ring-2 focus:ring-primary/30 focus:border-primary outline-none transition-all shadow-sm" })] }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsxs("div", { className: "flex items-center gap-3 bg-white dark:bg-neutral-950 border border-neutral-200 dark:border-neutral-800 rounded-xl px-4 py-2.5 shadow-sm", children: [_jsx(Grid3x3, { size: 16, className: "text-neutral-400 flex-shrink-0" }), _jsxs("div", { className: "flex items-center gap-3 min-w-[140px]", children: [_jsx("span", { className: "text-sm font-bold text-neutral-700 dark:text-neutral-300 min-w-[24px] text-right tabular-nums", children: columnCount }), _jsx("input", { type: "range", min: "3", max: "9", value: columnCount, onChange: (e) => setColumnCount(parseInt(e.target.value)), className: "flex-1 h-2 bg-neutral-200 dark:bg-neutral-800 rounded-lg appearance-none cursor-pointer accent-primary" })] }), _jsx("span", { className: "text-xs font-semibold text-neutral-500 dark:text-neutral-400 uppercase tracking-wider", children: "cols" })] }), _jsxs("button", { onClick: () => { var _a; return (_a = fileInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, disabled: uploading || internalUploading, className: "flex items-center justify-center gap-2 px-5 py-2.5 bg-primary text-white rounded-xl font-semibold text-sm hover:bg-primary/90 disabled:opacity-50 transition-all duration-200 shadow-md hover:shadow-lg active:scale-[0.98]", children: [(uploading || internalUploading) ? _jsx(Loader2, { size: 16, className: "animate-spin" }) : _jsx(Upload, { size: 16 }), "Upload"] })] })] }), _jsx("div", { ref: scrollContainerRef, className: "flex-1 overflow-y-auto p-6", style: { scrollbarWidth: 'thin' }, children: loading && images.length === 0 ? (_jsxs("div", { className: "h-full flex flex-col items-center justify-center space-y-4", children: [_jsx(Loader2, { size: 48, className: "text-primary animate-spin" }), _jsx("p", { className: "text-sm font-semibold text-neutral-500 uppercase tracking-wider", children: "Loading Library..." })] })) : images.length === 0 ? (_jsxs("div", { className: "h-full flex flex-col items-center justify-center text-center py-16", children: [_jsx("div", { className: "p-5 bg-gradient-to-br from-neutral-100 to-neutral-200 dark:from-neutral-800 dark:to-neutral-900 rounded-2xl mb-4 shadow-inner", children: _jsx(ImageIcon, { size: 36, className: "text-neutral-400" }) }), _jsx("h3", { className: "text-lg font-bold dark:text-white mb-2", children: "No images found" }), _jsx("p", { className: "text-sm text-neutral-500 max-w-sm", children: searchQuery.trim()
493
- ? 'Try a different search term or upload a new image.'
494
- : 'Upload your first image to get started.' })] })) : (_jsxs(_Fragment, { children: [_jsx(MasonryGrid, { images: images, selectedImageId: resolvedSelectedId, onSelectImage: (image) => {
495
- onSelectImage(image);
496
- }, onClose: onClose, fixedColumns: columnCount }), loadingMore && (_jsx("div", { className: "flex justify-center items-center py-8", children: _jsx(Loader2, { size: 24, className: "text-primary animate-spin" }) })), !hasMore && images.length > 0 && (_jsx("div", { className: "text-center py-6", children: _jsxs("p", { className: "text-xs text-neutral-400 font-semibold uppercase tracking-wider", children: ["All ", total, " images loaded"] }) }))] })) }), _jsxs("div", { className: "px-6 py-4 bg-gradient-to-r from-neutral-50 to-white dark:from-neutral-900 dark:to-neutral-950 border-t border-neutral-200 dark:border-neutral-800 flex flex-col sm:flex-row items-center gap-3", children: [_jsxs("div", { className: "flex items-center gap-2 text-neutral-400 min-w-max", children: [_jsx(LinkIcon, { size: 14 }), _jsx("span", { className: "text-[10px] font-bold uppercase tracking-wider", children: "External URL" })] }), _jsxs("div", { className: "flex-1 flex w-full gap-2", children: [_jsx("input", { type: "text", placeholder: "Paste image URL here...", value: externalUrl, onChange: (e) => setExternalUrl(e.target.value), onKeyDown: (e) => e.key === 'Enter' && handleUseExternalUrl(), className: "flex-1 px-3 py-2 bg-white dark:bg-neutral-950 border border-neutral-200 dark:border-neutral-800 rounded-lg text-xs font-medium focus:ring-2 focus:ring-primary/30 focus:border-primary outline-none transition-all shadow-sm" }), _jsx("button", { onClick: handleUseExternalUrl, className: "px-4 py-2 bg-neutral-900 dark:bg-white dark:text-neutral-900 text-white rounded-lg text-xs font-semibold hover:opacity-90 transition-all duration-200 active:scale-[0.98]", children: "Import" })] })] }), _jsx("input", { ref: fileInputRef, type: "file", accept: "image/*", onChange: (e) => {
497
- var _a;
498
- const file = (_a = e.target.files) === null || _a === void 0 ? void 0 : _a[0];
499
- if (file) {
500
- handleFileUpload(file);
501
- }
502
- }, className: "hidden" })] })] }));
503
- return createPortal(modalContent, portalTarget);
504
- }