@jhits/plugin-images 0.0.14 → 0.0.15
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/dist/components/Image.d.ts +1 -6
- package/dist/components/Image.d.ts.map +1 -1
- package/dist/components/Image.js +86 -202
- package/dist/components/ImageEditor.d.ts.map +1 -1
- package/dist/components/ImageEditor.js +21 -125
- package/dist/components/ImagePicker.d.ts.map +1 -1
- package/dist/components/ImagePicker.js +6 -59
- package/dist/utils/fallback.d.ts +9 -4
- package/dist/utils/fallback.d.ts.map +1 -1
- package/dist/utils/fallback.js +40 -12
- package/dist/utils/transforms.d.ts.map +1 -1
- package/dist/utils/transforms.js +7 -10
- package/dist/views/ImageManager/components/CleanupLibraryModal.d.ts +12 -0
- package/dist/views/ImageManager/components/CleanupLibraryModal.d.ts.map +1 -0
- package/dist/views/ImageManager/components/CleanupLibraryModal.js +7 -0
- package/dist/views/ImageManager/components/DeleteImageModal.d.ts +15 -0
- package/dist/views/ImageManager/components/DeleteImageModal.d.ts.map +1 -0
- package/dist/views/ImageManager/components/DeleteImageModal.js +8 -0
- package/dist/views/ImageManager/components/ImageGrid.d.ts +12 -0
- package/dist/views/ImageManager/components/ImageGrid.d.ts.map +1 -0
- package/dist/views/ImageManager/components/ImageGrid.js +15 -0
- package/dist/views/ImageManager/components/ImageManagerHeader.d.ts +11 -0
- package/dist/views/ImageManager/components/ImageManagerHeader.d.ts.map +1 -0
- package/dist/views/ImageManager/components/ImageManagerHeader.js +6 -0
- package/dist/views/ImageManager/components/ImageManagerStats.d.ts +8 -0
- package/dist/views/ImageManager/components/ImageManagerStats.d.ts.map +1 -0
- package/dist/views/ImageManager/components/ImageManagerStats.js +6 -0
- package/dist/views/ImageManager/components/ImageManagerToolbar.d.ts +9 -0
- package/dist/views/ImageManager/components/ImageManagerToolbar.d.ts.map +1 -0
- package/dist/views/ImageManager/components/ImageManagerToolbar.js +10 -0
- package/dist/views/ImageManager/components/ImageTable.d.ts +13 -0
- package/dist/views/ImageManager/components/ImageTable.d.ts.map +1 -0
- package/dist/views/ImageManager/components/ImageTable.js +13 -0
- package/dist/views/ImageManager/types.d.ts +26 -0
- package/dist/views/ImageManager/types.d.ts.map +1 -0
- package/dist/views/ImageManager/types.js +1 -0
- package/dist/views/ImageManager.d.ts +1 -1
- package/dist/views/ImageManager.d.ts.map +1 -1
- package/dist/views/ImageManager.js +28 -52
- package/package.json +10 -9
- package/src/components/Image.tsx +107 -262
- package/src/components/ImageEditor.tsx +31 -193
- package/src/components/ImagePicker.tsx +22 -107
- package/src/utils/fallback.ts +46 -13
- package/src/utils/transforms.ts +9 -12
- package/src/views/ImageManager/components/CleanupLibraryModal.tsx +96 -0
- package/src/views/ImageManager/components/DeleteImageModal.tsx +144 -0
- package/src/views/ImageManager/components/ImageGrid.tsx +119 -0
- package/src/views/ImageManager/components/ImageManagerHeader.tsx +72 -0
- package/src/views/ImageManager/components/ImageManagerStats.tsx +60 -0
- package/src/views/ImageManager/components/ImageManagerToolbar.tsx +60 -0
- package/src/views/ImageManager/components/ImageTable.tsx +120 -0
- package/src/views/ImageManager/types.ts +27 -0
- package/src/views/ImageManager.tsx +103 -571
- package/src/components/BackgroundImage.d.ts +0 -11
- package/src/components/BackgroundImage.d.ts.map +0 -1
- package/src/components/GlobalImageEditor/config.d.ts +0 -9
- package/src/components/GlobalImageEditor/config.d.ts.map +0 -1
- package/src/components/GlobalImageEditor/eventHandlers.d.ts +0 -20
- package/src/components/GlobalImageEditor/eventHandlers.d.ts.map +0 -1
- package/src/components/GlobalImageEditor/imageDetection.d.ts +0 -16
- package/src/components/GlobalImageEditor/imageDetection.d.ts.map +0 -1
- package/src/components/GlobalImageEditor/imageSetup.d.ts +0 -9
- package/src/components/GlobalImageEditor/imageSetup.d.ts.map +0 -1
- package/src/components/GlobalImageEditor/saveLogic.d.ts +0 -26
- package/src/components/GlobalImageEditor/saveLogic.d.ts.map +0 -1
- package/src/components/GlobalImageEditor/stylingDetection.d.ts +0 -9
- package/src/components/GlobalImageEditor/stylingDetection.d.ts.map +0 -1
- package/src/components/GlobalImageEditor/transformParsing.d.ts +0 -16
- package/src/components/GlobalImageEditor/transformParsing.d.ts.map +0 -1
- package/src/components/GlobalImageEditor/types.d.ts +0 -36
- package/src/components/GlobalImageEditor/types.d.ts.map +0 -1
- package/src/components/GlobalImageEditor.d.ts +0 -8
- package/src/components/GlobalImageEditor.d.ts.map +0 -1
- package/src/components/Image.d.ts +0 -22
- package/src/components/Image.d.ts.map +0 -1
- package/src/components/ImageBrowserModal.d.ts +0 -13
- package/src/components/ImageBrowserModal.d.ts.map +0 -1
- package/src/components/ImageEditor.d.ts +0 -27
- package/src/components/ImageEditor.d.ts.map +0 -1
- package/src/components/ImagePicker.d.ts +0 -3
- package/src/components/ImagePicker.d.ts.map +0 -1
- package/src/components/ImagesPluginInit.d.ts +0 -24
- package/src/components/ImagesPluginInit.d.ts.map +0 -1
- package/src/hooks/useImagePicker.d.ts +0 -20
- package/src/hooks/useImagePicker.d.ts.map +0 -1
- package/src/types/index.d.ts +0 -80
- package/src/types/index.d.ts.map +0 -1
- package/src/utils/fallback.d.ts +0 -27
- package/src/utils/fallback.d.ts.map +0 -1
- package/src/utils/transforms.d.ts +0 -26
- package/src/utils/transforms.d.ts.map +0 -1
- package/src/views/ImageManager.d.ts +0 -10
- package/src/views/ImageManager.d.ts.map +0 -1
|
@@ -1,60 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Image Manager View
|
|
3
|
-
*
|
|
3
|
+
* Refactored modular interface for media management
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
'use client';
|
|
7
7
|
|
|
8
8
|
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
|
9
9
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
Trash2,
|
|
13
|
-
Upload,
|
|
14
|
-
Image as ImageIcon,
|
|
15
|
-
Grid3x3,
|
|
16
|
-
List,
|
|
17
|
-
Loader2,
|
|
18
|
-
AlertTriangle,
|
|
19
|
-
CheckCircle,
|
|
20
|
-
ExternalLink,
|
|
21
|
-
X,
|
|
22
|
-
RefreshCw,
|
|
23
|
-
Eye,
|
|
24
|
-
HardDrive,
|
|
25
|
-
CheckCircle2,
|
|
26
|
-
Circle,
|
|
27
|
-
Trash
|
|
10
|
+
ImageIcon,
|
|
11
|
+
Loader2
|
|
28
12
|
} from 'lucide-react';
|
|
29
|
-
import Image from 'next/image';
|
|
30
|
-
|
|
31
|
-
interface ImageMetadata {
|
|
32
|
-
id: string;
|
|
33
|
-
filename: string;
|
|
34
|
-
url: string;
|
|
35
|
-
size: number;
|
|
36
|
-
mimeType: string;
|
|
37
|
-
uploadedAt: string;
|
|
38
|
-
}
|
|
39
13
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
url?: string;
|
|
49
|
-
}>;
|
|
50
|
-
}
|
|
14
|
+
// Components
|
|
15
|
+
import { ImageManagerHeader } from './ImageManager/components/ImageManagerHeader';
|
|
16
|
+
import { ImageManagerStats } from './ImageManager/components/ImageManagerStats';
|
|
17
|
+
import { ImageManagerToolbar } from './ImageManager/components/ImageManagerToolbar';
|
|
18
|
+
import { ImageGrid } from './ImageManager/components/ImageGrid';
|
|
19
|
+
import { ImageTable } from './ImageManager/components/ImageTable';
|
|
20
|
+
import { DeleteImageModal } from './ImageManager/components/DeleteImageModal';
|
|
21
|
+
import { CleanupLibraryModal } from './ImageManager/components/CleanupLibraryModal';
|
|
51
22
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
totalSize: number;
|
|
55
|
-
usedImages: number;
|
|
56
|
-
availableImages: number;
|
|
57
|
-
}
|
|
23
|
+
// Types
|
|
24
|
+
import { ImageMetadata, ImageUsage, StorageStats } from './ImageManager/types';
|
|
58
25
|
|
|
59
26
|
export interface ImageManagerViewProps {
|
|
60
27
|
siteId: string;
|
|
@@ -71,12 +38,10 @@ function formatFileSize(bytes: number): string {
|
|
|
71
38
|
|
|
72
39
|
function formatDate(dateString: string): string {
|
|
73
40
|
const date = new Date(dateString);
|
|
74
|
-
return date.toLocaleDateString(
|
|
41
|
+
return date.toLocaleDateString(undefined, {
|
|
75
42
|
year: 'numeric',
|
|
76
43
|
month: 'short',
|
|
77
|
-
day: 'numeric'
|
|
78
|
-
hour: '2-digit',
|
|
79
|
-
minute: '2-digit'
|
|
44
|
+
day: 'numeric'
|
|
80
45
|
});
|
|
81
46
|
}
|
|
82
47
|
|
|
@@ -158,6 +123,7 @@ export function ImageManagerView({ siteId, locale }: ImageManagerViewProps) {
|
|
|
158
123
|
setImages(prev => [data.image, ...prev]);
|
|
159
124
|
}
|
|
160
125
|
}
|
|
126
|
+
handleRefresh();
|
|
161
127
|
} catch (error) {
|
|
162
128
|
console.error('Upload failed:', error);
|
|
163
129
|
} finally {
|
|
@@ -180,6 +146,7 @@ export function ImageManagerView({ siteId, locale }: ImageManagerViewProps) {
|
|
|
180
146
|
setImages(prev => prev.filter(img => img.id !== selectedImage.id));
|
|
181
147
|
setShowDeleteModal(false);
|
|
182
148
|
setSelectedImage(null);
|
|
149
|
+
handleRefresh();
|
|
183
150
|
} else {
|
|
184
151
|
const error = await response.json();
|
|
185
152
|
alert(error.error || 'Failed to delete image');
|
|
@@ -217,6 +184,7 @@ export function ImageManagerView({ siteId, locale }: ImageManagerViewProps) {
|
|
|
217
184
|
|
|
218
185
|
if (deletedCount > 0) {
|
|
219
186
|
setImages(prev => prev.filter(img => isImageInUse(img.filename)));
|
|
187
|
+
handleRefresh();
|
|
220
188
|
}
|
|
221
189
|
|
|
222
190
|
setShowCleanupModal(false);
|
|
@@ -224,11 +192,11 @@ export function ImageManagerView({ siteId, locale }: ImageManagerViewProps) {
|
|
|
224
192
|
if (failedCount > 0) {
|
|
225
193
|
alert(`Deleted ${deletedCount} images. ${failedCount} failed.`);
|
|
226
194
|
} else {
|
|
227
|
-
alert(`Successfully deleted ${deletedCount} unused
|
|
195
|
+
alert(`Successfully deleted ${deletedCount} unused assets.`);
|
|
228
196
|
}
|
|
229
197
|
} catch (error) {
|
|
230
198
|
console.error('Cleanup failed:', error);
|
|
231
|
-
alert('Failed to cleanup
|
|
199
|
+
alert('Failed to cleanup assets');
|
|
232
200
|
} finally {
|
|
233
201
|
setCleaning(false);
|
|
234
202
|
}
|
|
@@ -262,532 +230,96 @@ export function ImageManagerView({ siteId, locale }: ImageManagerViewProps) {
|
|
|
262
230
|
};
|
|
263
231
|
|
|
264
232
|
return (
|
|
265
|
-
<div className="
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
<label className="inline-flex items-center gap-2 px-6 py-3 bg-primary text-white rounded-full text-[10px] font-black uppercase tracking-widest hover:bg-primary/90 transition-all shadow-lg shadow-primary/20 cursor-pointer">
|
|
290
|
-
{uploading ? (
|
|
291
|
-
<Loader2 size={16} className="animate-spin" />
|
|
292
|
-
) : (
|
|
293
|
-
<Upload size={16} />
|
|
294
|
-
)}
|
|
295
|
-
{uploading ? 'Uploading...' : 'Upload'}
|
|
296
|
-
<input
|
|
297
|
-
type="file"
|
|
298
|
-
accept="image/*"
|
|
299
|
-
multiple
|
|
300
|
-
onChange={handleUpload}
|
|
301
|
-
className="hidden"
|
|
302
|
-
disabled={uploading}
|
|
303
|
-
/>
|
|
304
|
-
</label>
|
|
305
|
-
|
|
306
|
-
<button
|
|
307
|
-
onClick={handleRefresh}
|
|
308
|
-
className="p-3 bg-neutral-100 dark:bg-neutral-800 text-neutral-600 dark:text-neutral-400 rounded-full hover:bg-neutral-200 dark:hover:bg-neutral-700 transition-colors"
|
|
309
|
-
title="Refresh"
|
|
310
|
-
>
|
|
311
|
-
<RefreshCw size={16} />
|
|
312
|
-
</button>
|
|
313
|
-
</div>
|
|
314
|
-
</div>
|
|
315
|
-
|
|
316
|
-
{/* Stats Cards */}
|
|
317
|
-
{stats && (
|
|
318
|
-
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
|
319
|
-
<div className="bg-neutral-100 dark:bg-neutral-800 rounded-2xl p-5 border border-neutral-200 dark:border-neutral-700">
|
|
320
|
-
<div className="flex items-center gap-3 mb-2">
|
|
321
|
-
<div className="p-2 bg-primary/10 rounded-xl">
|
|
322
|
-
<ImageIcon className="text-primary" size={20} />
|
|
323
|
-
</div>
|
|
324
|
-
<span className="text-xs font-bold text-neutral-500 dark:text-neutral-400 uppercase tracking-wider">Total Images</span>
|
|
325
|
-
</div>
|
|
326
|
-
<p className="text-2xl font-black text-neutral-900 dark:text-white">{stats.totalImages}</p>
|
|
327
|
-
</div>
|
|
328
|
-
|
|
329
|
-
<div className="bg-neutral-100 dark:bg-neutral-800 rounded-2xl p-5 border border-neutral-200 dark:border-neutral-700">
|
|
330
|
-
<div className="flex items-center gap-3 mb-2">
|
|
331
|
-
<div className="p-2 bg-amber-500/10 rounded-xl">
|
|
332
|
-
<HardDrive className="text-amber-500" size={20} />
|
|
333
|
-
</div>
|
|
334
|
-
<span className="text-xs font-bold text-neutral-500 dark:text-neutral-400 uppercase tracking-wider">Storage Used</span>
|
|
335
|
-
</div>
|
|
336
|
-
<p className="text-2xl font-black text-neutral-900 dark:text-white">{formatFileSize(stats.totalSize)}</p>
|
|
337
|
-
</div>
|
|
338
|
-
|
|
339
|
-
<div className="bg-neutral-100 dark:bg-neutral-800 rounded-2xl p-5 border border-neutral-200 dark:border-neutral-700">
|
|
340
|
-
<div className="flex items-center gap-3 mb-2">
|
|
341
|
-
<div className="p-2 bg-red-500/10 rounded-xl">
|
|
342
|
-
<CheckCircle2 className="text-red-500" size={20} />
|
|
343
|
-
</div>
|
|
344
|
-
<span className="text-xs font-bold text-neutral-500 dark:text-neutral-400 uppercase tracking-wider">In Use</span>
|
|
233
|
+
<div className="w-full flex flex-col space-y-8 px-6 lg:px-10 py-6 lg:py-10 pb-10 bg-transparent">
|
|
234
|
+
<ImageManagerHeader
|
|
235
|
+
uploading={uploading}
|
|
236
|
+
unusedCount={getUnusedImagesCount()}
|
|
237
|
+
onUpload={handleUpload}
|
|
238
|
+
onRefresh={handleRefresh}
|
|
239
|
+
onShowCleanup={() => setShowCleanupModal(true)}
|
|
240
|
+
/>
|
|
241
|
+
|
|
242
|
+
{stats && <ImageManagerStats stats={stats} formatFileSize={formatFileSize} />}
|
|
243
|
+
|
|
244
|
+
<ImageManagerToolbar
|
|
245
|
+
search={search}
|
|
246
|
+
setSearch={setSearch}
|
|
247
|
+
viewMode={viewMode}
|
|
248
|
+
setViewMode={setViewMode}
|
|
249
|
+
/>
|
|
250
|
+
|
|
251
|
+
<div className="flex-1 px-4 min-h-[400px]">
|
|
252
|
+
{loading ? (
|
|
253
|
+
<div className="h-full flex items-center justify-center py-20">
|
|
254
|
+
<div className="flex flex-col items-center gap-4">
|
|
255
|
+
<Loader2 size={40} className="animate-spin text-primary opacity-40" />
|
|
256
|
+
<p className="text-[10px] font-bold text-primary uppercase tracking-widest animate-pulse">Syncing Library</p>
|
|
345
257
|
</div>
|
|
346
|
-
<p className="text-2xl font-black text-neutral-900 dark:text-white">{stats.usedImages}</p>
|
|
347
258
|
</div>
|
|
348
|
-
|
|
349
|
-
<div className="
|
|
350
|
-
<div className="flex items-center
|
|
351
|
-
<
|
|
352
|
-
|
|
353
|
-
</div>
|
|
354
|
-
<span className="text-xs font-bold text-neutral-500 dark:text-neutral-400 uppercase tracking-wider">Available</span>
|
|
259
|
+
) : filteredImages.length === 0 ? (
|
|
260
|
+
<div className="h-full flex flex-col items-center justify-center py-32 text-center">
|
|
261
|
+
<div className="size-24 bg-dashboard-card/40 rounded-3xl border border-dashed border-dashboard-border/50 flex items-center justify-center mb-6 relative">
|
|
262
|
+
<ImageIcon size={40} className="text-dashboard-text-secondary opacity-20" />
|
|
263
|
+
<div className="absolute inset-0 bg-primary/5 rounded-3xl animate-pulse" />
|
|
355
264
|
</div>
|
|
356
|
-
<
|
|
265
|
+
<h3 className="text-xl font-bold text-dashboard-text tracking-tight opacity-40 mb-1.5">
|
|
266
|
+
{search ? 'Reference Not Found' : 'Repository Empty'}
|
|
267
|
+
</h3>
|
|
268
|
+
<p className="text-xs text-dashboard-text-secondary font-medium opacity-60">
|
|
269
|
+
{search ? 'Try refining your search parameters' : 'Upload your first image to begin building your library'}
|
|
270
|
+
</p>
|
|
357
271
|
</div>
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
onChange={(e) => setSearch(e.target.value)}
|
|
370
|
-
className="w-full pl-12 pr-4 py-3 bg-neutral-100 dark:bg-neutral-800/50 border border-neutral-300 dark:border-neutral-700 rounded-full text-sm focus:outline-none focus:ring-2 focus:ring-primary/50"
|
|
272
|
+
) : viewMode === 'grid' ? (
|
|
273
|
+
<ImageGrid
|
|
274
|
+
images={filteredImages}
|
|
275
|
+
isImageInUse={isImageInUse}
|
|
276
|
+
getImageUsage={getImageUsage}
|
|
277
|
+
formatFileSize={formatFileSize}
|
|
278
|
+
onViewFull={(url) => window.open(url, '_blank')}
|
|
279
|
+
onDelete={(img) => {
|
|
280
|
+
setSelectedImage(img);
|
|
281
|
+
setShowDeleteModal(true);
|
|
282
|
+
}}
|
|
371
283
|
/>
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
<button
|
|
387
|
-
onClick={() => setViewMode('list')}
|
|
388
|
-
className={`p-2 rounded-full transition-all ${
|
|
389
|
-
viewMode === 'list'
|
|
390
|
-
? 'bg-white dark:bg-neutral-900 text-primary shadow-sm'
|
|
391
|
-
: 'text-neutral-500 dark:text-neutral-400 hover:text-neutral-950 dark:hover:text-white'
|
|
392
|
-
}`}
|
|
393
|
-
title="List View"
|
|
394
|
-
>
|
|
395
|
-
<List size={18} />
|
|
396
|
-
</button>
|
|
397
|
-
</div>
|
|
284
|
+
) : (
|
|
285
|
+
<ImageTable
|
|
286
|
+
images={filteredImages}
|
|
287
|
+
isImageInUse={isImageInUse}
|
|
288
|
+
getImageUsage={getImageUsage}
|
|
289
|
+
formatFileSize={formatFileSize}
|
|
290
|
+
formatDate={formatDate}
|
|
291
|
+
onViewFull={(url) => window.open(url, '_blank')}
|
|
292
|
+
onDelete={(img) => {
|
|
293
|
+
setSelectedImage(img);
|
|
294
|
+
setShowDeleteModal(true);
|
|
295
|
+
}}
|
|
296
|
+
/>
|
|
297
|
+
)}
|
|
398
298
|
</div>
|
|
399
299
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
onChange={handleUpload}
|
|
424
|
-
className="hidden"
|
|
425
|
-
/>
|
|
426
|
-
</label>
|
|
427
|
-
)}
|
|
428
|
-
</div>
|
|
429
|
-
) : viewMode === 'grid' ? (
|
|
430
|
-
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
|
|
431
|
-
{filteredImages.map((image) => {
|
|
432
|
-
const usage = getImageUsage(image.filename);
|
|
433
|
-
const inUse = isImageInUse(image.filename);
|
|
434
|
-
|
|
435
|
-
return (
|
|
436
|
-
<div
|
|
437
|
-
key={image.id}
|
|
438
|
-
className={`group relative bg-neutral-100 dark:bg-neutral-800 rounded-2xl overflow-hidden border transition-all hover:shadow-lg ${
|
|
439
|
-
inUse
|
|
440
|
-
? 'border-amber-300 dark:border-amber-700'
|
|
441
|
-
: 'border-neutral-200 dark:border-neutral-700 hover:border-primary/50'
|
|
442
|
-
}`}
|
|
443
|
-
>
|
|
444
|
-
<div className="aspect-square relative bg-neutral-200 dark:bg-neutral-700">
|
|
445
|
-
<Image
|
|
446
|
-
src={image.url}
|
|
447
|
-
alt={image.filename}
|
|
448
|
-
fill
|
|
449
|
-
className="object-cover"
|
|
450
|
-
unoptimized
|
|
451
|
-
/>
|
|
452
|
-
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/40 transition-all flex items-center justify-center gap-2 opacity-0 group-hover:opacity-100">
|
|
453
|
-
<button
|
|
454
|
-
onClick={() => window.open(image.url, '_blank')}
|
|
455
|
-
className="p-2 bg-white dark:bg-neutral-900 rounded-full text-primary hover:bg-primary/10 transition-colors"
|
|
456
|
-
title="View full size"
|
|
457
|
-
>
|
|
458
|
-
<Eye size={16} />
|
|
459
|
-
</button>
|
|
460
|
-
<button
|
|
461
|
-
onClick={() => {
|
|
462
|
-
setSelectedImage(image);
|
|
463
|
-
setShowDeleteModal(true);
|
|
464
|
-
}}
|
|
465
|
-
className={`p-2 bg-white dark:bg-neutral-900 rounded-full transition-colors ${
|
|
466
|
-
inUse
|
|
467
|
-
? 'text-neutral-400 cursor-not-allowed'
|
|
468
|
-
: 'text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20'
|
|
469
|
-
}`}
|
|
470
|
-
title={inUse ? 'Cannot delete - image is in use' : 'Delete'}
|
|
471
|
-
disabled={inUse}
|
|
472
|
-
>
|
|
473
|
-
<Trash2 size={16} />
|
|
474
|
-
</button>
|
|
475
|
-
</div>
|
|
476
|
-
{inUse && (
|
|
477
|
-
<div className="absolute top-2 right-2 px-2 py-1 bg-red-500 text-white text-[10px] font-bold uppercase rounded-full flex items-center gap-1 shadow-lg">
|
|
478
|
-
<CheckCircle size={10} />
|
|
479
|
-
In Use
|
|
480
|
-
</div>
|
|
481
|
-
)}
|
|
482
|
-
</div>
|
|
483
|
-
<div className="p-3">
|
|
484
|
-
<p className="text-xs font-medium text-neutral-700 dark:text-neutral-300 truncate" title={image.filename}>
|
|
485
|
-
{image.filename}
|
|
486
|
-
</p>
|
|
487
|
-
<div className="flex items-center justify-between mt-1">
|
|
488
|
-
<p className="text-[10px] text-neutral-500">
|
|
489
|
-
{formatFileSize(image.size)}
|
|
490
|
-
</p>
|
|
491
|
-
{inUse ? (
|
|
492
|
-
<p className="text-[10px] text-red-500 dark:text-red-400 font-bold">
|
|
493
|
-
{usage.length > 0 ? `${usage.length} ref${usage.length > 1 ? 's' : ''}` : 'mapped'}
|
|
494
|
-
</p>
|
|
495
|
-
) : (
|
|
496
|
-
<p className="text-[10px] text-green-500 dark:text-green-400 font-bold">
|
|
497
|
-
available
|
|
498
|
-
</p>
|
|
499
|
-
)}
|
|
500
|
-
</div>
|
|
501
|
-
</div>
|
|
502
|
-
</div>
|
|
503
|
-
);
|
|
504
|
-
})}
|
|
505
|
-
</div>
|
|
506
|
-
) : (
|
|
507
|
-
<div className="bg-neutral-100 dark:bg-neutral-800 rounded-2xl overflow-hidden border border-neutral-200 dark:border-neutral-700">
|
|
508
|
-
<table className="w-full">
|
|
509
|
-
<thead className="bg-neutral-200 dark:bg-neutral-700/50">
|
|
510
|
-
<tr>
|
|
511
|
-
<th className="text-left px-4 py-3 text-xs font-bold text-neutral-600 dark:text-neutral-400 uppercase">Preview</th>
|
|
512
|
-
<th className="text-left px-4 py-3 text-xs font-bold text-neutral-600 dark:text-neutral-400 uppercase">Filename</th>
|
|
513
|
-
<th className="text-left px-4 py-3 text-xs font-bold text-neutral-600 dark:text-neutral-400 uppercase">Size</th>
|
|
514
|
-
<th className="text-left px-4 py-3 text-xs font-bold text-neutral-600 dark:text-neutral-400 uppercase">Uploaded</th>
|
|
515
|
-
<th className="text-left px-4 py-3 text-xs font-bold text-neutral-600 dark:text-neutral-400 uppercase">Status</th>
|
|
516
|
-
<th className="text-right px-4 py-3 text-xs font-bold text-neutral-600 dark:text-neutral-400 uppercase">Actions</th>
|
|
517
|
-
</tr>
|
|
518
|
-
</thead>
|
|
519
|
-
<tbody className="divide-y divide-neutral-200 dark:divide-neutral-700">
|
|
520
|
-
{filteredImages.map((image) => {
|
|
521
|
-
const usage = getImageUsage(image.filename);
|
|
522
|
-
const inUse = isImageInUse(image.filename);
|
|
523
|
-
|
|
524
|
-
return (
|
|
525
|
-
<tr key={image.id} className={`transition-colors ${
|
|
526
|
-
inUse
|
|
527
|
-
? 'bg-amber-50/30 dark:bg-amber-900/5 hover:bg-amber-50/50 dark:hover:bg-amber-900/10'
|
|
528
|
-
: 'hover:bg-neutral-200/50 dark:hover:bg-neutral-700/30'
|
|
529
|
-
}`}>
|
|
530
|
-
<td className="px-4 py-3">
|
|
531
|
-
<div className={`w-12 h-12 relative bg-neutral-200 dark:bg-neutral-700 rounded-lg overflow-hidden ring-2 ${
|
|
532
|
-
inUse ? 'ring-red-400 dark:ring-red-600' : 'ring-transparent'
|
|
533
|
-
}`}>
|
|
534
|
-
<Image
|
|
535
|
-
src={image.url}
|
|
536
|
-
alt={image.filename}
|
|
537
|
-
fill
|
|
538
|
-
className="object-cover"
|
|
539
|
-
unoptimized
|
|
540
|
-
/>
|
|
541
|
-
</div>
|
|
542
|
-
</td>
|
|
543
|
-
<td className="px-4 py-3">
|
|
544
|
-
<p className="text-sm font-medium text-neutral-700 dark:text-neutral-300 truncate max-w-[200px]" title={image.filename}>
|
|
545
|
-
{image.filename}
|
|
546
|
-
</p>
|
|
547
|
-
</td>
|
|
548
|
-
<td className="px-4 py-3 text-sm text-neutral-600 dark:text-neutral-400">
|
|
549
|
-
{formatFileSize(image.size)}
|
|
550
|
-
</td>
|
|
551
|
-
<td className="px-4 py-3 text-sm text-neutral-600 dark:text-neutral-400">
|
|
552
|
-
{formatDate(image.uploadedAt)}
|
|
553
|
-
</td>
|
|
554
|
-
<td className="px-4 py-3">
|
|
555
|
-
{inUse ? (
|
|
556
|
-
<span className="inline-flex items-center gap-1 px-2 py-1 bg-red-500/10 text-red-600 dark:text-red-400 text-xs font-bold rounded-full">
|
|
557
|
-
<CheckCircle size={12} />
|
|
558
|
-
In Use{usage.length > 0 ? ` (${usage.length})` : ''}
|
|
559
|
-
</span>
|
|
560
|
-
) : (
|
|
561
|
-
<span className="inline-flex items-center gap-1 px-2 py-1 bg-green-500/10 text-green-600 dark:text-green-400 text-xs font-medium rounded-full">
|
|
562
|
-
<Circle size={12} />
|
|
563
|
-
Available
|
|
564
|
-
</span>
|
|
565
|
-
)}
|
|
566
|
-
</td>
|
|
567
|
-
<td className="px-4 py-3 text-right">
|
|
568
|
-
<div className="flex items-center justify-end gap-2">
|
|
569
|
-
<button
|
|
570
|
-
onClick={() => window.open(image.url, '_blank')}
|
|
571
|
-
className="p-2 text-neutral-500 hover:text-primary hover:bg-neutral-200 dark:hover:bg-neutral-700 rounded-lg transition-colors"
|
|
572
|
-
title="View"
|
|
573
|
-
>
|
|
574
|
-
<Eye size={16} />
|
|
575
|
-
</button>
|
|
576
|
-
<button
|
|
577
|
-
onClick={() => {
|
|
578
|
-
setSelectedImage(image);
|
|
579
|
-
setShowDeleteModal(true);
|
|
580
|
-
}}
|
|
581
|
-
className={`p-2 rounded-lg transition-colors ${
|
|
582
|
-
inUse
|
|
583
|
-
? 'text-neutral-400 cursor-not-allowed'
|
|
584
|
-
: 'text-neutral-500 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20'
|
|
585
|
-
}`}
|
|
586
|
-
title={inUse ? 'Cannot delete - image is in use' : 'Delete'}
|
|
587
|
-
disabled={inUse}
|
|
588
|
-
>
|
|
589
|
-
<Trash2 size={16} />
|
|
590
|
-
</button>
|
|
591
|
-
</div>
|
|
592
|
-
</td>
|
|
593
|
-
</tr>
|
|
594
|
-
);
|
|
595
|
-
})}
|
|
596
|
-
</tbody>
|
|
597
|
-
</table>
|
|
598
|
-
</div>
|
|
599
|
-
)}
|
|
600
|
-
|
|
601
|
-
{/* Delete Confirmation Modal */}
|
|
602
|
-
{showDeleteModal && selectedImage && (
|
|
603
|
-
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
|
604
|
-
<div className="bg-white dark:bg-neutral-900 rounded-2xl p-6 max-w-md w-full mx-4 shadow-2xl">
|
|
605
|
-
<div className="flex items-center justify-between mb-4">
|
|
606
|
-
<h3 className="text-xl font-bold text-neutral-900 dark:text-white">Delete Image</h3>
|
|
607
|
-
<button
|
|
608
|
-
onClick={() => {
|
|
609
|
-
setShowDeleteModal(false);
|
|
610
|
-
setSelectedImage(null);
|
|
611
|
-
}}
|
|
612
|
-
className="p-2 text-neutral-500 hover:text-neutral-700 dark:hover:text-neutral-300 rounded-lg"
|
|
613
|
-
>
|
|
614
|
-
<X size={20} />
|
|
615
|
-
</button>
|
|
616
|
-
</div>
|
|
617
|
-
|
|
618
|
-
{(() => {
|
|
619
|
-
const usage = getImageUsage(selectedImage.filename);
|
|
620
|
-
const inUse = isImageInUse(selectedImage.filename);
|
|
621
|
-
const isMapped = isImageMapped(selectedImage.filename);
|
|
622
|
-
|
|
623
|
-
return (
|
|
624
|
-
<>
|
|
625
|
-
{inUse ? (
|
|
626
|
-
<div className="mb-6">
|
|
627
|
-
<div className="flex items-center gap-2 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-xl mb-4">
|
|
628
|
-
<AlertTriangle className="text-red-500 shrink-0" size={24} />
|
|
629
|
-
<div>
|
|
630
|
-
<p className="text-sm font-medium text-red-800 dark:text-red-200">
|
|
631
|
-
{isMapped && usage.length === 0
|
|
632
|
-
? 'This image is mapped to content'
|
|
633
|
-
: 'This image is currently in use'}
|
|
634
|
-
</p>
|
|
635
|
-
<p className="text-xs text-red-600 dark:text-red-400 mt-1">
|
|
636
|
-
{isMapped && usage.length === 0
|
|
637
|
-
? 'The image is linked via semantic ID mapping.'
|
|
638
|
-
: 'Please remove the image from all content before deleting.'}
|
|
639
|
-
</p>
|
|
640
|
-
</div>
|
|
641
|
-
</div>
|
|
642
|
-
|
|
643
|
-
{(usage.length > 0) && (
|
|
644
|
-
<div className="space-y-2 max-h-[200px] overflow-y-auto">
|
|
645
|
-
<p className="text-sm font-medium text-neutral-700 dark:text-neutral-300">Used in:</p>
|
|
646
|
-
{usage.map((item, idx) => (
|
|
647
|
-
<div key={idx} className="flex items-center justify-between p-3 bg-neutral-100 dark:bg-neutral-800 rounded-lg">
|
|
648
|
-
<div>
|
|
649
|
-
<p className="text-sm font-medium text-neutral-700 dark:text-neutral-300">{item.title}</p>
|
|
650
|
-
<p className="text-xs text-neutral-500">{item.plugin} - {item.type}</p>
|
|
651
|
-
</div>
|
|
652
|
-
{item.url && (
|
|
653
|
-
<a
|
|
654
|
-
href={item.url}
|
|
655
|
-
target="_blank"
|
|
656
|
-
rel="noopener noreferrer"
|
|
657
|
-
className="p-2 text-primary hover:bg-primary/10 rounded-lg transition-colors"
|
|
658
|
-
>
|
|
659
|
-
<ExternalLink size={16} />
|
|
660
|
-
</a>
|
|
661
|
-
)}
|
|
662
|
-
</div>
|
|
663
|
-
))}
|
|
664
|
-
</div>
|
|
665
|
-
)}
|
|
666
|
-
|
|
667
|
-
{isMapped && usage.length === 0 && (
|
|
668
|
-
<p className="text-sm text-neutral-600 dark:text-neutral-400 mt-2">
|
|
669
|
-
This image is being used through semantic ID mappings. Delete the mapping first.
|
|
670
|
-
</p>
|
|
671
|
-
)}
|
|
672
|
-
</div>
|
|
673
|
-
) : (
|
|
674
|
-
<div className="mb-6">
|
|
675
|
-
<div className="flex items-center gap-4 mb-4">
|
|
676
|
-
<div className="w-20 h-20 relative bg-neutral-200 dark:bg-neutral-700 rounded-lg overflow-hidden shrink-0">
|
|
677
|
-
<Image
|
|
678
|
-
src={selectedImage.url}
|
|
679
|
-
alt={selectedImage.filename}
|
|
680
|
-
fill
|
|
681
|
-
className="object-cover"
|
|
682
|
-
unoptimized
|
|
683
|
-
/>
|
|
684
|
-
</div>
|
|
685
|
-
<div>
|
|
686
|
-
<p className="font-medium text-neutral-900 dark:text-white">{selectedImage.filename}</p>
|
|
687
|
-
<p className="text-sm text-neutral-500">{formatFileSize(selectedImage.size)}</p>
|
|
688
|
-
<p className="text-sm text-neutral-500">{formatDate(selectedImage.uploadedAt)}</p>
|
|
689
|
-
</div>
|
|
690
|
-
</div>
|
|
691
|
-
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
|
692
|
-
Are you sure you want to delete this image? This action cannot be undone.
|
|
693
|
-
</p>
|
|
694
|
-
</div>
|
|
695
|
-
)}
|
|
696
|
-
|
|
697
|
-
<div className="flex justify-end gap-3">
|
|
698
|
-
<button
|
|
699
|
-
onClick={() => {
|
|
700
|
-
setShowDeleteModal(false);
|
|
701
|
-
setSelectedImage(null);
|
|
702
|
-
}}
|
|
703
|
-
className="px-4 py-2 text-neutral-600 dark:text-neutral-400 hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-lg transition-colors"
|
|
704
|
-
>
|
|
705
|
-
Cancel
|
|
706
|
-
</button>
|
|
707
|
-
{!inUse && (
|
|
708
|
-
<button
|
|
709
|
-
onClick={handleDelete}
|
|
710
|
-
disabled={deleting}
|
|
711
|
-
className="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors disabled:opacity-50 flex items-center gap-2"
|
|
712
|
-
>
|
|
713
|
-
{deleting && <Loader2 size={16} className="animate-spin" />}
|
|
714
|
-
Delete
|
|
715
|
-
</button>
|
|
716
|
-
)}
|
|
717
|
-
</div>
|
|
718
|
-
</>
|
|
719
|
-
);
|
|
720
|
-
})()}
|
|
721
|
-
</div>
|
|
722
|
-
</div>
|
|
723
|
-
)}
|
|
724
|
-
|
|
725
|
-
{/* Cleanup Confirmation Modal */}
|
|
726
|
-
{showCleanupModal && (
|
|
727
|
-
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
|
728
|
-
<div className="bg-white dark:bg-neutral-900 rounded-2xl p-6 max-w-md w-full mx-4 shadow-2xl">
|
|
729
|
-
<div className="flex items-center justify-between mb-4">
|
|
730
|
-
<h3 className="text-xl font-bold text-neutral-900 dark:text-white">Clean Up Unused Images</h3>
|
|
731
|
-
<button
|
|
732
|
-
onClick={() => setShowCleanupModal(false)}
|
|
733
|
-
className="p-2 text-neutral-500 hover:text-neutral-700 dark:hover:text-neutral-300 rounded-lg"
|
|
734
|
-
>
|
|
735
|
-
<X size={20} />
|
|
736
|
-
</button>
|
|
737
|
-
</div>
|
|
738
|
-
|
|
739
|
-
<div className="mb-6">
|
|
740
|
-
<div className="flex items-center gap-2 p-4 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-xl mb-4">
|
|
741
|
-
<AlertTriangle className="text-amber-500 shrink-0" size={24} />
|
|
742
|
-
<div>
|
|
743
|
-
<p className="text-sm font-medium text-amber-800 dark:text-amber-200">
|
|
744
|
-
Warning: This action cannot be undone
|
|
745
|
-
</p>
|
|
746
|
-
<p className="text-xs text-amber-600 dark:text-amber-400 mt-1">
|
|
747
|
-
{getUnusedImagesCount()} unused image{getUnusedImagesCount() !== 1 ? 's' : ''} will be permanently deleted.
|
|
748
|
-
</p>
|
|
749
|
-
</div>
|
|
750
|
-
</div>
|
|
751
|
-
|
|
752
|
-
<p className="text-sm text-neutral-600 dark:text-neutral-400 mb-4">
|
|
753
|
-
This will delete all images that are not currently in use by any content (blogs, newsletters, or user avatars).
|
|
754
|
-
</p>
|
|
755
|
-
|
|
756
|
-
<div className="p-3 bg-neutral-100 dark:bg-neutral-800 rounded-lg">
|
|
757
|
-
<div className="flex items-center justify-between text-sm">
|
|
758
|
-
<span className="text-neutral-600 dark:text-neutral-400">Total images:</span>
|
|
759
|
-
<span className="font-bold text-neutral-900 dark:text-white">{stats?.totalImages || 0}</span>
|
|
760
|
-
</div>
|
|
761
|
-
<div className="flex items-center justify-between text-sm mt-2">
|
|
762
|
-
<span className="text-neutral-600 dark:text-neutral-400">In use:</span>
|
|
763
|
-
<span className="font-bold text-red-500">{stats?.usedImages || 0}</span>
|
|
764
|
-
</div>
|
|
765
|
-
<div className="flex items-center justify-between text-sm mt-2">
|
|
766
|
-
<span className="text-neutral-600 dark:text-neutral-400">Will be deleted:</span>
|
|
767
|
-
<span className="font-bold text-red-500">{getUnusedImagesCount()}</span>
|
|
768
|
-
</div>
|
|
769
|
-
</div>
|
|
770
|
-
</div>
|
|
771
|
-
|
|
772
|
-
<div className="flex justify-end gap-3">
|
|
773
|
-
<button
|
|
774
|
-
onClick={() => setShowCleanupModal(false)}
|
|
775
|
-
className="px-4 py-2 text-neutral-600 dark:text-neutral-400 hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-lg transition-colors"
|
|
776
|
-
>
|
|
777
|
-
Cancel
|
|
778
|
-
</button>
|
|
779
|
-
<button
|
|
780
|
-
onClick={handleCleanup}
|
|
781
|
-
disabled={cleaning}
|
|
782
|
-
className="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors disabled:opacity-50 flex items-center gap-2"
|
|
783
|
-
>
|
|
784
|
-
{cleaning && <Loader2 size={16} className="animate-spin" />}
|
|
785
|
-
{cleaning ? 'Deleting...' : `Delete ${getUnusedImagesCount()} Images`}
|
|
786
|
-
</button>
|
|
787
|
-
</div>
|
|
788
|
-
</div>
|
|
789
|
-
</div>
|
|
790
|
-
)}
|
|
300
|
+
<DeleteImageModal
|
|
301
|
+
isOpen={showDeleteModal}
|
|
302
|
+
image={selectedImage}
|
|
303
|
+
inUse={selectedImage ? isImageInUse(selectedImage.filename) : false}
|
|
304
|
+
usage={selectedImage ? getImageUsage(selectedImage.filename) : []}
|
|
305
|
+
isDeleting={deleting}
|
|
306
|
+
formatFileSize={formatFileSize}
|
|
307
|
+
formatDate={formatDate}
|
|
308
|
+
onClose={() => {
|
|
309
|
+
setShowDeleteModal(false);
|
|
310
|
+
setSelectedImage(null);
|
|
311
|
+
}}
|
|
312
|
+
onConfirm={handleDelete}
|
|
313
|
+
/>
|
|
314
|
+
|
|
315
|
+
<CleanupLibraryModal
|
|
316
|
+
isOpen={showCleanupModal}
|
|
317
|
+
stats={stats}
|
|
318
|
+
unusedCount={getUnusedImagesCount()}
|
|
319
|
+
isCleaning={cleaning}
|
|
320
|
+
onClose={() => setShowCleanupModal(false)}
|
|
321
|
+
onConfirm={handleCleanup}
|
|
322
|
+
/>
|
|
791
323
|
</div>
|
|
792
324
|
);
|
|
793
325
|
}
|