@jhits/plugin-images 0.0.13 → 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/api/list/index.d.ts +18 -0
- package/dist/api/list/index.d.ts.map +1 -1
- package/dist/api/list/index.js +121 -20
- package/dist/api/router.d.ts.map +1 -1
- package/dist/api/router.js +7 -0
- package/dist/api/usage/route.d.ts +23 -0
- package/dist/api/usage/route.d.ts.map +1 -0
- package/dist/api/usage/route.js +238 -0
- package/dist/components/BackgroundImage.d.ts.map +1 -1
- package/dist/components/BackgroundImage.js +5 -17
- package/dist/components/GlobalImageEditor.d.ts.map +1 -1
- package/dist/components/GlobalImageEditor.js +9 -4
- package/dist/components/Image.d.ts +3 -6
- package/dist/components/Image.d.ts.map +1 -1
- package/dist/components/Image.js +103 -206
- 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 +206 -2
- package/package.json +10 -9
- package/src/api/list/index.ts +147 -22
- package/src/api/router.ts +8 -0
- package/src/api/usage/route.ts +294 -0
- package/src/components/BackgroundImage.tsx +5 -15
- package/src/components/GlobalImageEditor.tsx +9 -4
- package/src/components/Image.tsx +128 -268
- 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 +307 -12
- 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
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { Search, Grid3x3, List } from 'lucide-react';
|
|
5
|
+
|
|
6
|
+
interface ImageManagerToolbarProps {
|
|
7
|
+
search: string;
|
|
8
|
+
setSearch: (value: string) => void;
|
|
9
|
+
viewMode: 'grid' | 'list';
|
|
10
|
+
setViewMode: (mode: 'grid' | 'list') => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function ImageManagerToolbar({
|
|
14
|
+
search,
|
|
15
|
+
setSearch,
|
|
16
|
+
viewMode,
|
|
17
|
+
setViewMode
|
|
18
|
+
}: ImageManagerToolbarProps) {
|
|
19
|
+
return (
|
|
20
|
+
<div className="px-4">
|
|
21
|
+
<div className="flex flex-col sm:flex-row items-center gap-4 p-2 bg-dashboard-card/30 backdrop-blur-xl border border-dashboard-border/40 rounded-2xl shadow-sm">
|
|
22
|
+
<div className="relative flex-1 group w-full">
|
|
23
|
+
<Search className="absolute left-4 top-1/2 -translate-y-1/2 text-dashboard-text-secondary/40 group-focus-within:text-primary transition-colors duration-300" size={18} />
|
|
24
|
+
<input
|
|
25
|
+
type="text"
|
|
26
|
+
placeholder="Search assets by filename..."
|
|
27
|
+
value={search}
|
|
28
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
29
|
+
className="w-full pl-12 pr-6 py-3 bg-transparent border-none outline-none text-sm font-semibold text-dashboard-text placeholder:text-dashboard-text-secondary/30"
|
|
30
|
+
/>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<div className="flex items-center gap-1.5 p-1.5 bg-dashboard-bg/50 rounded-xl border border-dashboard-border/40">
|
|
34
|
+
<button
|
|
35
|
+
onClick={() => setViewMode('grid')}
|
|
36
|
+
className={`p-2.5 rounded-lg transition-all ${
|
|
37
|
+
viewMode === 'grid'
|
|
38
|
+
? 'bg-primary text-white shadow-lg shadow-primary/20'
|
|
39
|
+
: 'text-dashboard-text-secondary hover:text-primary hover:bg-primary/5'
|
|
40
|
+
}`}
|
|
41
|
+
title="Grid View"
|
|
42
|
+
>
|
|
43
|
+
<Grid3x3 size={18} />
|
|
44
|
+
</button>
|
|
45
|
+
<button
|
|
46
|
+
onClick={() => setViewMode('list')}
|
|
47
|
+
className={`p-2.5 rounded-lg transition-all ${
|
|
48
|
+
viewMode === 'list'
|
|
49
|
+
? 'bg-primary text-white shadow-lg shadow-primary/20'
|
|
50
|
+
: 'text-dashboard-text-secondary hover:text-primary hover:bg-primary/5'
|
|
51
|
+
}`}
|
|
52
|
+
title="List View"
|
|
53
|
+
>
|
|
54
|
+
<List size={18} />
|
|
55
|
+
</button>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import Image from 'next/image';
|
|
5
|
+
import { Eye, Trash2, Activity, Circle, CheckCircle2 } from 'lucide-react';
|
|
6
|
+
import { ImageMetadata } from '../types';
|
|
7
|
+
|
|
8
|
+
interface ImageTableProps {
|
|
9
|
+
images: ImageMetadata[];
|
|
10
|
+
isImageInUse: (filename: string) => boolean;
|
|
11
|
+
getImageUsage: (filename: string) => any[];
|
|
12
|
+
formatFileSize: (bytes: number) => string;
|
|
13
|
+
formatDate: (date: string) => string;
|
|
14
|
+
onViewFull: (url: string) => void;
|
|
15
|
+
onDelete: (image: ImageMetadata) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function ImageTable({
|
|
19
|
+
images,
|
|
20
|
+
isImageInUse,
|
|
21
|
+
getImageUsage,
|
|
22
|
+
formatFileSize,
|
|
23
|
+
formatDate,
|
|
24
|
+
onViewFull,
|
|
25
|
+
onDelete
|
|
26
|
+
}: ImageTableProps) {
|
|
27
|
+
return (
|
|
28
|
+
<div className="bg-dashboard-card/40 rounded-2xl overflow-hidden border border-dashboard-border/40 shadow-sm">
|
|
29
|
+
<table className="w-full text-left border-collapse">
|
|
30
|
+
<thead>
|
|
31
|
+
<tr className="text-[10px] font-bold text-dashboard-text-secondary uppercase tracking-widest border-b border-dashboard-border/40 bg-dashboard-bg/20">
|
|
32
|
+
<th className="px-8 py-5">Asset Reference</th>
|
|
33
|
+
<th className="px-8 py-5">File Size</th>
|
|
34
|
+
<th className="px-8 py-5">Uploaded</th>
|
|
35
|
+
<th className="px-8 py-5">Usage Status</th>
|
|
36
|
+
<th className="px-8 py-5 text-right">Actions</th>
|
|
37
|
+
</tr>
|
|
38
|
+
</thead>
|
|
39
|
+
<tbody className="divide-y divide-dashboard-border/30">
|
|
40
|
+
{images.map((image) => {
|
|
41
|
+
const usage = getImageUsage(image.filename);
|
|
42
|
+
const inUse = isImageInUse(image.filename);
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<tr key={image.id} className="group hover:bg-primary/[0.03] transition-colors">
|
|
46
|
+
<td className="px-8 py-4">
|
|
47
|
+
<div className="flex items-center gap-4">
|
|
48
|
+
<div className={`size-12 relative bg-dashboard-bg/50 rounded-xl overflow-hidden border transition-all ${
|
|
49
|
+
inUse ? 'border-primary/30' : 'border-dashboard-border/60'
|
|
50
|
+
}`}>
|
|
51
|
+
<Image
|
|
52
|
+
src={image.url}
|
|
53
|
+
alt={image.filename}
|
|
54
|
+
fill
|
|
55
|
+
className="object-cover"
|
|
56
|
+
unoptimized
|
|
57
|
+
/>
|
|
58
|
+
</div>
|
|
59
|
+
<div className="min-w-0">
|
|
60
|
+
<p className="font-bold text-dashboard-text text-sm tracking-tight group-hover:text-primary transition-colors truncate max-w-[250px]" title={image.filename}>
|
|
61
|
+
{image.filename}
|
|
62
|
+
</p>
|
|
63
|
+
<p className="text-[9px] font-medium text-dashboard-text-secondary opacity-60 uppercase tracking-widest">Media Asset</p>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</td>
|
|
67
|
+
<td className="px-8 py-4">
|
|
68
|
+
<span className="text-xs font-semibold text-dashboard-text/70">
|
|
69
|
+
{formatFileSize(image.size)}
|
|
70
|
+
</span>
|
|
71
|
+
</td>
|
|
72
|
+
<td className="px-8 py-4">
|
|
73
|
+
<span className="text-xs font-semibold text-dashboard-text/70">
|
|
74
|
+
{formatDate(image.uploadedAt)}
|
|
75
|
+
</span>
|
|
76
|
+
</td>
|
|
77
|
+
<td className="px-8 py-4">
|
|
78
|
+
{inUse ? (
|
|
79
|
+
<span className="inline-flex items-center gap-2 px-3 py-1 bg-emerald-500/10 text-emerald-600 border border-emerald-500/20 rounded-full text-[9px] font-bold uppercase tracking-wider">
|
|
80
|
+
<CheckCircle2 size={10} />
|
|
81
|
+
Active{usage.length > 0 ? ` (${usage.length})` : ''}
|
|
82
|
+
</span>
|
|
83
|
+
) : (
|
|
84
|
+
<span className="inline-flex items-center gap-2 px-3 py-1 bg-dashboard-bg/50 border border-dashboard-border/60 text-dashboard-text-secondary/60 rounded-full text-[9px] font-bold uppercase tracking-wider">
|
|
85
|
+
<Circle size={10} />
|
|
86
|
+
Unlinked
|
|
87
|
+
</span>
|
|
88
|
+
)}
|
|
89
|
+
</td>
|
|
90
|
+
<td className="px-8 py-4 text-right">
|
|
91
|
+
<div className="flex items-center justify-end gap-1.5">
|
|
92
|
+
<button
|
|
93
|
+
onClick={() => onViewFull(image.url)}
|
|
94
|
+
className="p-2.5 text-dashboard-text-secondary hover:text-primary hover:bg-primary/10 rounded-lg transition-all active:scale-90"
|
|
95
|
+
title="View Image"
|
|
96
|
+
>
|
|
97
|
+
<Eye size={18} />
|
|
98
|
+
</button>
|
|
99
|
+
<button
|
|
100
|
+
onClick={() => onDelete(image)}
|
|
101
|
+
className={`p-2.5 rounded-lg transition-all active:scale-90 ${
|
|
102
|
+
inUse
|
|
103
|
+
? 'text-neutral-300 cursor-not-allowed opacity-20'
|
|
104
|
+
: 'text-dashboard-text-secondary hover:text-red-500 hover:bg-red-500/10'
|
|
105
|
+
}`}
|
|
106
|
+
disabled={inUse}
|
|
107
|
+
title="Delete Image"
|
|
108
|
+
>
|
|
109
|
+
<Trash2 size={18} />
|
|
110
|
+
</button>
|
|
111
|
+
</div>
|
|
112
|
+
</td>
|
|
113
|
+
</tr>
|
|
114
|
+
);
|
|
115
|
+
})}
|
|
116
|
+
</tbody>
|
|
117
|
+
</table>
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface ImageMetadata {
|
|
2
|
+
id: string;
|
|
3
|
+
filename: string;
|
|
4
|
+
url: string;
|
|
5
|
+
size: number;
|
|
6
|
+
mimeType: string;
|
|
7
|
+
uploadedAt: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ImageUsage {
|
|
11
|
+
imageId: string;
|
|
12
|
+
filename: string;
|
|
13
|
+
usage: Array<{
|
|
14
|
+
plugin: string;
|
|
15
|
+
type: string;
|
|
16
|
+
title: string;
|
|
17
|
+
id: string;
|
|
18
|
+
url?: string;
|
|
19
|
+
}>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface StorageStats {
|
|
23
|
+
totalImages: number;
|
|
24
|
+
totalSize: number;
|
|
25
|
+
usedImages: number;
|
|
26
|
+
availableImages: number;
|
|
27
|
+
}
|
|
@@ -1,30 +1,325 @@
|
|
|
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
|
-
import React from 'react';
|
|
8
|
+
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
|
9
|
+
import {
|
|
10
|
+
ImageIcon,
|
|
11
|
+
Loader2
|
|
12
|
+
} from 'lucide-react';
|
|
13
|
+
|
|
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';
|
|
22
|
+
|
|
23
|
+
// Types
|
|
24
|
+
import { ImageMetadata, ImageUsage, StorageStats } from './ImageManager/types';
|
|
9
25
|
|
|
10
26
|
export interface ImageManagerViewProps {
|
|
11
27
|
siteId: string;
|
|
12
28
|
locale: string;
|
|
13
29
|
}
|
|
14
30
|
|
|
31
|
+
function formatFileSize(bytes: number): string {
|
|
32
|
+
if (bytes === 0) return '0 B';
|
|
33
|
+
const k = 1024;
|
|
34
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
35
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
36
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function formatDate(dateString: string): string {
|
|
40
|
+
const date = new Date(dateString);
|
|
41
|
+
return date.toLocaleDateString(undefined, {
|
|
42
|
+
year: 'numeric',
|
|
43
|
+
month: 'short',
|
|
44
|
+
day: 'numeric'
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
15
48
|
export function ImageManagerView({ siteId, locale }: ImageManagerViewProps) {
|
|
49
|
+
const [images, setImages] = useState<ImageMetadata[]>([]);
|
|
50
|
+
const [usageData, setUsageData] = useState<Record<string, ImageUsage['usage']>>({});
|
|
51
|
+
const [stats, setStats] = useState<StorageStats | null>(null);
|
|
52
|
+
const [mappedImages, setMappedImages] = useState<Set<string>>(new Set());
|
|
53
|
+
const [loading, setLoading] = useState(true);
|
|
54
|
+
const [search, setSearch] = useState('');
|
|
55
|
+
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
|
|
56
|
+
const [uploading, setUploading] = useState(false);
|
|
57
|
+
const [selectedImage, setSelectedImage] = useState<ImageMetadata | null>(null);
|
|
58
|
+
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
|
59
|
+
const [showCleanupModal, setShowCleanupModal] = useState(false);
|
|
60
|
+
const [deleting, setDeleting] = useState(false);
|
|
61
|
+
const [cleaning, setCleaning] = useState(false);
|
|
62
|
+
const [refreshKey, setRefreshKey] = useState(0);
|
|
63
|
+
|
|
64
|
+
const fetchImages = useCallback(async () => {
|
|
65
|
+
try {
|
|
66
|
+
setLoading(true);
|
|
67
|
+
const response = await fetch('/api/plugin-images/list');
|
|
68
|
+
const data = await response.json();
|
|
69
|
+
|
|
70
|
+
if (data.images) {
|
|
71
|
+
setImages(data.images);
|
|
72
|
+
setStats(data.stats || null);
|
|
73
|
+
setMappedImages(new Set(data.mappedImages || []));
|
|
74
|
+
}
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error('Failed to fetch images:', error);
|
|
77
|
+
} finally {
|
|
78
|
+
setLoading(false);
|
|
79
|
+
}
|
|
80
|
+
}, []);
|
|
81
|
+
|
|
82
|
+
const fetchUsageData = useCallback(async () => {
|
|
83
|
+
try {
|
|
84
|
+
const response = await fetch('/api/plugin-images/usage');
|
|
85
|
+
const data = await response.json();
|
|
86
|
+
|
|
87
|
+
if (data.images) {
|
|
88
|
+
const usageMap: Record<string, ImageUsage['usage']> = {};
|
|
89
|
+
data.images.forEach((img: ImageUsage) => {
|
|
90
|
+
usageMap[img.filename] = img.usage;
|
|
91
|
+
});
|
|
92
|
+
setUsageData(usageMap);
|
|
93
|
+
}
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error('Failed to fetch usage data:', error);
|
|
96
|
+
}
|
|
97
|
+
}, []);
|
|
98
|
+
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
fetchImages();
|
|
101
|
+
fetchUsageData();
|
|
102
|
+
}, [fetchImages, fetchUsageData, refreshKey]);
|
|
103
|
+
|
|
104
|
+
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
105
|
+
const files = e.target.files;
|
|
106
|
+
if (!files || files.length === 0) return;
|
|
107
|
+
|
|
108
|
+
setUploading(true);
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
for (const file of Array.from(files)) {
|
|
112
|
+
const formData = new FormData();
|
|
113
|
+
formData.append('file', file);
|
|
114
|
+
|
|
115
|
+
const response = await fetch('/api/plugin-images/upload', {
|
|
116
|
+
method: 'POST',
|
|
117
|
+
body: formData,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const data = await response.json();
|
|
121
|
+
|
|
122
|
+
if (data.success && data.image) {
|
|
123
|
+
setImages(prev => [data.image, ...prev]);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
handleRefresh();
|
|
127
|
+
} catch (error) {
|
|
128
|
+
console.error('Upload failed:', error);
|
|
129
|
+
} finally {
|
|
130
|
+
setUploading(false);
|
|
131
|
+
if (e.target) e.target.value = '';
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const handleDelete = async () => {
|
|
136
|
+
if (!selectedImage) return;
|
|
137
|
+
|
|
138
|
+
setDeleting(true);
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
const response = await fetch(`/api/plugin-images/uploads/${selectedImage.filename}`, {
|
|
142
|
+
method: 'DELETE',
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
if (response.ok) {
|
|
146
|
+
setImages(prev => prev.filter(img => img.id !== selectedImage.id));
|
|
147
|
+
setShowDeleteModal(false);
|
|
148
|
+
setSelectedImage(null);
|
|
149
|
+
handleRefresh();
|
|
150
|
+
} else {
|
|
151
|
+
const error = await response.json();
|
|
152
|
+
alert(error.error || 'Failed to delete image');
|
|
153
|
+
}
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error('Delete failed:', error);
|
|
156
|
+
alert('Failed to delete image');
|
|
157
|
+
} finally {
|
|
158
|
+
setDeleting(false);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const handleCleanup = async () => {
|
|
163
|
+
setCleaning(true);
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const unusedImages = images.filter(img => !isImageInUse(img.filename));
|
|
167
|
+
let deletedCount = 0;
|
|
168
|
+
let failedCount = 0;
|
|
169
|
+
|
|
170
|
+
for (const image of unusedImages) {
|
|
171
|
+
try {
|
|
172
|
+
const response = await fetch(`/api/plugin-images/uploads/${image.filename}`, {
|
|
173
|
+
method: 'DELETE',
|
|
174
|
+
});
|
|
175
|
+
if (response.ok) {
|
|
176
|
+
deletedCount++;
|
|
177
|
+
} else {
|
|
178
|
+
failedCount++;
|
|
179
|
+
}
|
|
180
|
+
} catch {
|
|
181
|
+
failedCount++;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (deletedCount > 0) {
|
|
186
|
+
setImages(prev => prev.filter(img => isImageInUse(img.filename)));
|
|
187
|
+
handleRefresh();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
setShowCleanupModal(false);
|
|
191
|
+
|
|
192
|
+
if (failedCount > 0) {
|
|
193
|
+
alert(`Deleted ${deletedCount} images. ${failedCount} failed.`);
|
|
194
|
+
} else {
|
|
195
|
+
alert(`Successfully deleted ${deletedCount} unused assets.`);
|
|
196
|
+
}
|
|
197
|
+
} catch (error) {
|
|
198
|
+
console.error('Cleanup failed:', error);
|
|
199
|
+
alert('Failed to cleanup assets');
|
|
200
|
+
} finally {
|
|
201
|
+
setCleaning(false);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const getUnusedImagesCount = (): number => {
|
|
206
|
+
return images.filter(img => !isImageInUse(img.filename)).length;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const handleRefresh = () => {
|
|
210
|
+
setRefreshKey(prev => prev + 1);
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const filteredImages = useMemo(() => {
|
|
214
|
+
return images.filter(img =>
|
|
215
|
+
search === '' ||
|
|
216
|
+
img.filename.toLowerCase().includes(search.toLowerCase())
|
|
217
|
+
);
|
|
218
|
+
}, [images, search]);
|
|
219
|
+
|
|
220
|
+
const getImageUsage = (filename: string): ImageUsage['usage'] => {
|
|
221
|
+
return usageData[filename] || [];
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const isImageMapped = (filename: string): boolean => {
|
|
225
|
+
return mappedImages.has(filename);
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const isImageInUse = (filename: string): boolean => {
|
|
229
|
+
return isImageMapped(filename) || getImageUsage(filename).length > 0;
|
|
230
|
+
};
|
|
231
|
+
|
|
16
232
|
return (
|
|
17
|
-
<div className="
|
|
18
|
-
<
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
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" />
|
|
264
|
+
</div>
|
|
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>
|
|
271
|
+
</div>
|
|
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
|
+
}}
|
|
283
|
+
/>
|
|
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
|
+
)}
|
|
26
298
|
</div>
|
|
299
|
+
|
|
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
|
+
/>
|
|
27
323
|
</div>
|
|
28
324
|
);
|
|
29
325
|
}
|
|
30
|
-
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
export interface BackgroundImageProps {
|
|
3
|
-
id: string;
|
|
4
|
-
className?: string;
|
|
5
|
-
style?: React.CSSProperties;
|
|
6
|
-
children?: React.ReactNode;
|
|
7
|
-
backgroundSize?: 'cover' | 'contain' | 'auto' | string;
|
|
8
|
-
backgroundPosition?: string;
|
|
9
|
-
}
|
|
10
|
-
export declare function BackgroundImage({ id, className, style, children, backgroundSize, backgroundPosition, }: BackgroundImageProps): import("react/jsx-runtime").JSX.Element;
|
|
11
|
-
//# sourceMappingURL=BackgroundImage.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"BackgroundImage.d.ts","sourceRoot":"","sources":["BackgroundImage.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA8B,MAAM,OAAO,CAAC;AAInD,MAAM,WAAW,oBAAoB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAC5B,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,cAAc,CAAC,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;IACvD,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,wBAAgB,eAAe,CAAC,EAC5B,EAAE,EACF,SAAc,EACd,KAAU,EACV,QAAQ,EACR,cAAwB,EACxB,kBAA6B,GAChC,EAAE,oBAAoB,2CAqEtB"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["config.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE5C;;GAEG;AACH,wBAAgB,eAAe,IAAI,YAAY,CAW9C"}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Event handlers for image changes and effects
|
|
3
|
-
*/
|
|
4
|
-
import type { SelectedImage } from './types';
|
|
5
|
-
import type { ImageMetadata } from '../../types';
|
|
6
|
-
/**
|
|
7
|
-
* Apply image changes to DOM and save to API
|
|
8
|
-
*/
|
|
9
|
-
export declare function handleImageChange(image: ImageMetadata | null, selectedImage: SelectedImage, onClose: () => void): Promise<void>;
|
|
10
|
-
/**
|
|
11
|
-
* Handle brightness change
|
|
12
|
-
* @param saveImmediately - If true, save to API immediately. If false, only update state/DOM (for editor preview)
|
|
13
|
-
*/
|
|
14
|
-
export declare function handleBrightnessChange(brightness: number, selectedImage: SelectedImage, setSelectedImage: (image: SelectedImage) => void, saveImmediately?: boolean): Promise<void>;
|
|
15
|
-
/**
|
|
16
|
-
* Handle blur change
|
|
17
|
-
* @param saveImmediately - If true, save to API immediately. If false, only update state/DOM (for editor preview)
|
|
18
|
-
*/
|
|
19
|
-
export declare function handleBlurChange(blur: number, selectedImage: SelectedImage, setSelectedImage: (image: SelectedImage) => void, saveImmediately?: boolean): Promise<void>;
|
|
20
|
-
//# sourceMappingURL=eventHandlers.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"eventHandlers.d.ts","sourceRoot":"","sources":["eventHandlers.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAGjD;;GAEG;AACH,wBAAsB,iBAAiB,CACnC,KAAK,EAAE,aAAa,GAAG,IAAI,EAC3B,aAAa,EAAE,aAAa,EAC5B,OAAO,EAAE,MAAM,IAAI,GACpB,OAAO,CAAC,IAAI,CAAC,CAiDf;AAoID;;;GAGG;AACH,wBAAsB,sBAAsB,CACxC,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,aAAa,EAC5B,gBAAgB,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,EAChD,eAAe,GAAE,OAAc,GAChC,OAAO,CAAC,IAAI,CAAC,CAyBf;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CAClC,IAAI,EAAE,MAAM,EACZ,aAAa,EAAE,aAAa,EAC5B,gBAAgB,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,EAChD,eAAe,GAAE,OAAc,GAChC,OAAO,CAAC,IAAI,CAAC,CAyBf"}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Image detection and parsing utilities
|
|
3
|
-
*/
|
|
4
|
-
import type { SelectedImage } from './types';
|
|
5
|
-
/**
|
|
6
|
-
* Extract image source from element (handles Next.js Image wrapper)
|
|
7
|
-
*/
|
|
8
|
-
export declare function getImageSource(element: HTMLElement, isBackground: boolean): {
|
|
9
|
-
currentSrc: string;
|
|
10
|
-
actualImgElement: HTMLImageElement | null;
|
|
11
|
-
};
|
|
12
|
-
/**
|
|
13
|
-
* Parse image data from element to create SelectedImage
|
|
14
|
-
*/
|
|
15
|
-
export declare function parseImageData(element: HTMLElement, id: string, currentBrightness?: number, currentBlur?: number): SelectedImage | null;
|
|
16
|
-
//# sourceMappingURL=imageDetection.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"imageDetection.d.ts","sourceRoot":"","sources":["imageDetection.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAI7C;;GAEG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,OAAO,GAAG;IACzE,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,gBAAgB,GAAG,IAAI,CAAC;CAC7C,CAmCA;AAED;;GAEG;AACH,wBAAgB,cAAc,CAC1B,OAAO,EAAE,WAAW,EACpB,EAAE,EAAE,MAAM,EACV,iBAAiB,CAAC,EAAE,MAAM,EAC1B,WAAW,CAAC,EAAE,MAAM,GACrB,aAAa,GAAG,IAAI,CAoGtB"}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Image setup and click handlers
|
|
3
|
-
*/
|
|
4
|
-
import type { SelectedImage } from './types';
|
|
5
|
-
/**
|
|
6
|
-
* Setup click handlers and hover effects for all images
|
|
7
|
-
*/
|
|
8
|
-
export declare function setupImageHandlers(onImageClick: (image: SelectedImage) => void): () => void;
|
|
9
|
-
//# sourceMappingURL=imageSetup.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"imageSetup.d.ts","sourceRoot":"","sources":["imageSetup.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAK7C;;GAEG;AACH,wBAAgB,kBAAkB,CAC9B,YAAY,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,GAC7C,MAAM,IAAI,CAmSZ"}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Save logic for image transforms and effects
|
|
3
|
-
*/
|
|
4
|
-
import type { SelectedImage, PendingTransform } from './types';
|
|
5
|
-
/**
|
|
6
|
-
* Normalize position values - convert -50% (centering value) to 0
|
|
7
|
-
*/
|
|
8
|
-
export declare function normalizePosition(position: number): number;
|
|
9
|
-
/**
|
|
10
|
-
* Get filename from element or API
|
|
11
|
-
*/
|
|
12
|
-
export declare function getFilename(semanticId: string, selectedImage: SelectedImage): Promise<string | null>;
|
|
13
|
-
/**
|
|
14
|
-
* Save image transform to API
|
|
15
|
-
*/
|
|
16
|
-
export declare function saveTransformToAPI(semanticId: string, filename: string, scale: number, positionX: number, positionY: number, brightness: number, blur: number): Promise<boolean>;
|
|
17
|
-
/**
|
|
18
|
-
* Flush pending save immediately
|
|
19
|
-
*/
|
|
20
|
-
export declare function flushPendingSave(pending: PendingTransform, selectedImage: SelectedImage): Promise<void>;
|
|
21
|
-
/**
|
|
22
|
-
* Save image transform with debouncing support
|
|
23
|
-
* This is a wrapper that handles the debouncing logic
|
|
24
|
-
*/
|
|
25
|
-
export declare function saveImageTransform(semanticId: string, filename: string, scale: number, positionX: number, positionY: number, brightness: number, blur: number): Promise<void>;
|
|
26
|
-
//# sourceMappingURL=saveLogic.d.ts.map
|