@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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ImageManager.d.ts","sourceRoot":"","sources":["../../src/views/ImageManager.tsx"],"names":[],"mappings":"AAAA;;;GAGG;
|
|
1
|
+
{"version":3,"file":"ImageManager.d.ts","sourceRoot":"","sources":["../../src/views/ImageManager.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAsBH,MAAM,WAAW,qBAAqB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAClB;AAmBD,wBAAgB,gBAAgB,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,qBAAqB,2CAqRzE"}
|
|
@@ -1,9 +1,213 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Image Manager View
|
|
3
|
-
*
|
|
3
|
+
* Refactored modular interface for media management
|
|
4
4
|
*/
|
|
5
5
|
'use client';
|
|
6
6
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
7
|
+
import { useState, useEffect, useMemo, useCallback } from 'react';
|
|
8
|
+
import { ImageIcon, Loader2 } from 'lucide-react';
|
|
9
|
+
// Components
|
|
10
|
+
import { ImageManagerHeader } from './ImageManager/components/ImageManagerHeader';
|
|
11
|
+
import { ImageManagerStats } from './ImageManager/components/ImageManagerStats';
|
|
12
|
+
import { ImageManagerToolbar } from './ImageManager/components/ImageManagerToolbar';
|
|
13
|
+
import { ImageGrid } from './ImageManager/components/ImageGrid';
|
|
14
|
+
import { ImageTable } from './ImageManager/components/ImageTable';
|
|
15
|
+
import { DeleteImageModal } from './ImageManager/components/DeleteImageModal';
|
|
16
|
+
import { CleanupLibraryModal } from './ImageManager/components/CleanupLibraryModal';
|
|
17
|
+
function formatFileSize(bytes) {
|
|
18
|
+
if (bytes === 0)
|
|
19
|
+
return '0 B';
|
|
20
|
+
const k = 1024;
|
|
21
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
22
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
23
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
24
|
+
}
|
|
25
|
+
function formatDate(dateString) {
|
|
26
|
+
const date = new Date(dateString);
|
|
27
|
+
return date.toLocaleDateString(undefined, {
|
|
28
|
+
year: 'numeric',
|
|
29
|
+
month: 'short',
|
|
30
|
+
day: 'numeric'
|
|
31
|
+
});
|
|
32
|
+
}
|
|
7
33
|
export function ImageManagerView({ siteId, locale }) {
|
|
8
|
-
|
|
34
|
+
const [images, setImages] = useState([]);
|
|
35
|
+
const [usageData, setUsageData] = useState({});
|
|
36
|
+
const [stats, setStats] = useState(null);
|
|
37
|
+
const [mappedImages, setMappedImages] = useState(new Set());
|
|
38
|
+
const [loading, setLoading] = useState(true);
|
|
39
|
+
const [search, setSearch] = useState('');
|
|
40
|
+
const [viewMode, setViewMode] = useState('grid');
|
|
41
|
+
const [uploading, setUploading] = useState(false);
|
|
42
|
+
const [selectedImage, setSelectedImage] = useState(null);
|
|
43
|
+
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
|
44
|
+
const [showCleanupModal, setShowCleanupModal] = useState(false);
|
|
45
|
+
const [deleting, setDeleting] = useState(false);
|
|
46
|
+
const [cleaning, setCleaning] = useState(false);
|
|
47
|
+
const [refreshKey, setRefreshKey] = useState(0);
|
|
48
|
+
const fetchImages = useCallback(async () => {
|
|
49
|
+
try {
|
|
50
|
+
setLoading(true);
|
|
51
|
+
const response = await fetch('/api/plugin-images/list');
|
|
52
|
+
const data = await response.json();
|
|
53
|
+
if (data.images) {
|
|
54
|
+
setImages(data.images);
|
|
55
|
+
setStats(data.stats || null);
|
|
56
|
+
setMappedImages(new Set(data.mappedImages || []));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
console.error('Failed to fetch images:', error);
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
setLoading(false);
|
|
64
|
+
}
|
|
65
|
+
}, []);
|
|
66
|
+
const fetchUsageData = useCallback(async () => {
|
|
67
|
+
try {
|
|
68
|
+
const response = await fetch('/api/plugin-images/usage');
|
|
69
|
+
const data = await response.json();
|
|
70
|
+
if (data.images) {
|
|
71
|
+
const usageMap = {};
|
|
72
|
+
data.images.forEach((img) => {
|
|
73
|
+
usageMap[img.filename] = img.usage;
|
|
74
|
+
});
|
|
75
|
+
setUsageData(usageMap);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
console.error('Failed to fetch usage data:', error);
|
|
80
|
+
}
|
|
81
|
+
}, []);
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
fetchImages();
|
|
84
|
+
fetchUsageData();
|
|
85
|
+
}, [fetchImages, fetchUsageData, refreshKey]);
|
|
86
|
+
const handleUpload = async (e) => {
|
|
87
|
+
const files = e.target.files;
|
|
88
|
+
if (!files || files.length === 0)
|
|
89
|
+
return;
|
|
90
|
+
setUploading(true);
|
|
91
|
+
try {
|
|
92
|
+
for (const file of Array.from(files)) {
|
|
93
|
+
const formData = new FormData();
|
|
94
|
+
formData.append('file', file);
|
|
95
|
+
const response = await fetch('/api/plugin-images/upload', {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
body: formData,
|
|
98
|
+
});
|
|
99
|
+
const data = await response.json();
|
|
100
|
+
if (data.success && data.image) {
|
|
101
|
+
setImages(prev => [data.image, ...prev]);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
handleRefresh();
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
console.error('Upload failed:', error);
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
setUploading(false);
|
|
111
|
+
if (e.target)
|
|
112
|
+
e.target.value = '';
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
const handleDelete = async () => {
|
|
116
|
+
if (!selectedImage)
|
|
117
|
+
return;
|
|
118
|
+
setDeleting(true);
|
|
119
|
+
try {
|
|
120
|
+
const response = await fetch(`/api/plugin-images/uploads/${selectedImage.filename}`, {
|
|
121
|
+
method: 'DELETE',
|
|
122
|
+
});
|
|
123
|
+
if (response.ok) {
|
|
124
|
+
setImages(prev => prev.filter(img => img.id !== selectedImage.id));
|
|
125
|
+
setShowDeleteModal(false);
|
|
126
|
+
setSelectedImage(null);
|
|
127
|
+
handleRefresh();
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
const error = await response.json();
|
|
131
|
+
alert(error.error || 'Failed to delete image');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
console.error('Delete failed:', error);
|
|
136
|
+
alert('Failed to delete image');
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
setDeleting(false);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
const handleCleanup = async () => {
|
|
143
|
+
setCleaning(true);
|
|
144
|
+
try {
|
|
145
|
+
const unusedImages = images.filter(img => !isImageInUse(img.filename));
|
|
146
|
+
let deletedCount = 0;
|
|
147
|
+
let failedCount = 0;
|
|
148
|
+
for (const image of unusedImages) {
|
|
149
|
+
try {
|
|
150
|
+
const response = await fetch(`/api/plugin-images/uploads/${image.filename}`, {
|
|
151
|
+
method: 'DELETE',
|
|
152
|
+
});
|
|
153
|
+
if (response.ok) {
|
|
154
|
+
deletedCount++;
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
failedCount++;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
failedCount++;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (deletedCount > 0) {
|
|
165
|
+
setImages(prev => prev.filter(img => isImageInUse(img.filename)));
|
|
166
|
+
handleRefresh();
|
|
167
|
+
}
|
|
168
|
+
setShowCleanupModal(false);
|
|
169
|
+
if (failedCount > 0) {
|
|
170
|
+
alert(`Deleted ${deletedCount} images. ${failedCount} failed.`);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
alert(`Successfully deleted ${deletedCount} unused assets.`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
console.error('Cleanup failed:', error);
|
|
178
|
+
alert('Failed to cleanup assets');
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
setCleaning(false);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
const getUnusedImagesCount = () => {
|
|
185
|
+
return images.filter(img => !isImageInUse(img.filename)).length;
|
|
186
|
+
};
|
|
187
|
+
const handleRefresh = () => {
|
|
188
|
+
setRefreshKey(prev => prev + 1);
|
|
189
|
+
};
|
|
190
|
+
const filteredImages = useMemo(() => {
|
|
191
|
+
return images.filter(img => search === '' ||
|
|
192
|
+
img.filename.toLowerCase().includes(search.toLowerCase()));
|
|
193
|
+
}, [images, search]);
|
|
194
|
+
const getImageUsage = (filename) => {
|
|
195
|
+
return usageData[filename] || [];
|
|
196
|
+
};
|
|
197
|
+
const isImageMapped = (filename) => {
|
|
198
|
+
return mappedImages.has(filename);
|
|
199
|
+
};
|
|
200
|
+
const isImageInUse = (filename) => {
|
|
201
|
+
return isImageMapped(filename) || getImageUsage(filename).length > 0;
|
|
202
|
+
};
|
|
203
|
+
return (_jsxs("div", { className: "w-full flex flex-col space-y-8 px-6 lg:px-10 py-6 lg:py-10 pb-10 bg-transparent", children: [_jsx(ImageManagerHeader, { uploading: uploading, unusedCount: getUnusedImagesCount(), onUpload: handleUpload, onRefresh: handleRefresh, onShowCleanup: () => setShowCleanupModal(true) }), stats && _jsx(ImageManagerStats, { stats: stats, formatFileSize: formatFileSize }), _jsx(ImageManagerToolbar, { search: search, setSearch: setSearch, viewMode: viewMode, setViewMode: setViewMode }), _jsx("div", { className: "flex-1 px-4 min-h-[400px]", children: loading ? (_jsx("div", { className: "h-full flex items-center justify-center py-20", children: _jsxs("div", { className: "flex flex-col items-center gap-4", children: [_jsx(Loader2, { size: 40, className: "animate-spin text-primary opacity-40" }), _jsx("p", { className: "text-[10px] font-bold text-primary uppercase tracking-widest animate-pulse", children: "Syncing Library" })] }) })) : filteredImages.length === 0 ? (_jsxs("div", { className: "h-full flex flex-col items-center justify-center py-32 text-center", children: [_jsxs("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", children: [_jsx(ImageIcon, { size: 40, className: "text-dashboard-text-secondary opacity-20" }), _jsx("div", { className: "absolute inset-0 bg-primary/5 rounded-3xl animate-pulse" })] }), _jsx("h3", { className: "text-xl font-bold text-dashboard-text tracking-tight opacity-40 mb-1.5", children: search ? 'Reference Not Found' : 'Repository Empty' }), _jsx("p", { className: "text-xs text-dashboard-text-secondary font-medium opacity-60", children: search ? 'Try refining your search parameters' : 'Upload your first image to begin building your library' })] })) : viewMode === 'grid' ? (_jsx(ImageGrid, { images: filteredImages, isImageInUse: isImageInUse, getImageUsage: getImageUsage, formatFileSize: formatFileSize, onViewFull: (url) => window.open(url, '_blank'), onDelete: (img) => {
|
|
204
|
+
setSelectedImage(img);
|
|
205
|
+
setShowDeleteModal(true);
|
|
206
|
+
} })) : (_jsx(ImageTable, { images: filteredImages, isImageInUse: isImageInUse, getImageUsage: getImageUsage, formatFileSize: formatFileSize, formatDate: formatDate, onViewFull: (url) => window.open(url, '_blank'), onDelete: (img) => {
|
|
207
|
+
setSelectedImage(img);
|
|
208
|
+
setShowDeleteModal(true);
|
|
209
|
+
} })) }), _jsx(DeleteImageModal, { isOpen: showDeleteModal, image: selectedImage, inUse: selectedImage ? isImageInUse(selectedImage.filename) : false, usage: selectedImage ? getImageUsage(selectedImage.filename) : [], isDeleting: deleting, formatFileSize: formatFileSize, formatDate: formatDate, onClose: () => {
|
|
210
|
+
setShowDeleteModal(false);
|
|
211
|
+
setSelectedImage(null);
|
|
212
|
+
}, onConfirm: handleDelete }), _jsx(CleanupLibraryModal, { isOpen: showCleanupModal, stats: stats, unusedCount: getUnusedImagesCount(), isCleaning: cleaning, onClose: () => setShowCleanupModal(false), onConfirm: handleCleanup })] }));
|
|
9
213
|
}
|
package/package.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jhits/plugin-images",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.15",
|
|
4
4
|
"description": "Image management and storage plugin for the JHITS ecosystem",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
8
|
-
"main": "./
|
|
9
|
-
"types": "./
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
10
|
"exports": {
|
|
11
11
|
".": {
|
|
12
|
-
"types": "./
|
|
13
|
-
"import": "./
|
|
14
|
-
"default": "./
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
15
|
},
|
|
16
16
|
"./src": {
|
|
17
17
|
"types": "./src/index.tsx",
|
|
@@ -19,12 +19,13 @@
|
|
|
19
19
|
"default": "./src/index.tsx"
|
|
20
20
|
},
|
|
21
21
|
"./server": {
|
|
22
|
-
"types": "./
|
|
23
|
-
"import": "./
|
|
24
|
-
"default": "./
|
|
22
|
+
"types": "./dist/index.server.d.ts",
|
|
23
|
+
"import": "./dist/index.server.js",
|
|
24
|
+
"default": "./dist/index.server.js"
|
|
25
25
|
}
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
+
"framer-motion": "^12.36.0",
|
|
28
29
|
"lucide-react": "^0.564.0",
|
|
29
30
|
"mongodb": "^7.1.0",
|
|
30
31
|
"@jhits/plugin-core": "0.0.10"
|
package/src/api/list/index.ts
CHANGED
|
@@ -4,65 +4,183 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { NextRequest, NextResponse } from 'next/server';
|
|
7
|
-
import { readdir, stat } from 'fs/promises';
|
|
7
|
+
import { readdir, stat, readFile } from 'fs/promises';
|
|
8
8
|
import path from 'path';
|
|
9
9
|
|
|
10
10
|
const uploadsDir = path.join(process.cwd(), 'data', 'uploads');
|
|
11
|
+
const mappingsPath = path.join(process.cwd(), 'data', 'image-mappings.json');
|
|
12
|
+
|
|
13
|
+
interface BlogDoc {
|
|
14
|
+
_id: string;
|
|
15
|
+
slug?: string;
|
|
16
|
+
title?: string;
|
|
17
|
+
image?: { id?: string; src?: string };
|
|
18
|
+
contentBlocks?: any[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface UserDoc {
|
|
22
|
+
_id: string;
|
|
23
|
+
name?: string;
|
|
24
|
+
email?: string;
|
|
25
|
+
image?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function queryDatabase(): Promise<{ blogFilenames: Set<string>; userFilenames: Set<string> }> {
|
|
29
|
+
const blogFilenames = new Set<string>();
|
|
30
|
+
const userFilenames = new Set<string>();
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const { MongoClient } = await import('mongodb');
|
|
34
|
+
const uri = process.env.DATABASE_URL || process.env.MONGODB_URI;
|
|
35
|
+
if (!uri) return { blogFilenames, userFilenames };
|
|
36
|
+
|
|
37
|
+
const client = new MongoClient(uri);
|
|
38
|
+
await client.connect();
|
|
39
|
+
const db = client.db();
|
|
40
|
+
|
|
41
|
+
// Get blogs
|
|
42
|
+
const blogs = await db.collection('blogs').find({}).project({ image: 1, contentBlocks: 1 }).toArray() as BlogDoc[];
|
|
43
|
+
|
|
44
|
+
for (const blog of blogs) {
|
|
45
|
+
// Featured image
|
|
46
|
+
if (blog.image?.id) blogFilenames.add(blog.image.id);
|
|
47
|
+
if (blog.image?.src) blogFilenames.add(blog.image.src);
|
|
48
|
+
|
|
49
|
+
// Content blocks - recursively find image.src
|
|
50
|
+
if (blog.contentBlocks) {
|
|
51
|
+
const findImages = (obj: any) => {
|
|
52
|
+
if (!obj || typeof obj !== 'object') return;
|
|
53
|
+
if (Array.isArray(obj)) {
|
|
54
|
+
obj.forEach(findImages);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (obj.src && typeof obj.src === 'string' && !obj.src.startsWith('http')) {
|
|
58
|
+
blogFilenames.add(obj.src);
|
|
59
|
+
}
|
|
60
|
+
if (obj.children) findImages(obj.children);
|
|
61
|
+
if (obj.data) findImages(obj.data);
|
|
62
|
+
};
|
|
63
|
+
findImages(blog.contentBlocks);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Get users with avatars
|
|
68
|
+
const users = await db.collection('users').find({ image: { $exists: true, $ne: null } }).project({ image: 1 }).toArray() as UserDoc[];
|
|
69
|
+
for (const user of users) {
|
|
70
|
+
if (user.image && user.image.includes('/api/uploads/')) {
|
|
71
|
+
const filename = user.image.split('/api/uploads/')[1];
|
|
72
|
+
if (filename) userFilenames.add(filename);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
await client.close();
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error('Error querying database for list:', error);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { blogFilenames, userFilenames };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function extractMappings(): { mappedFilenames: Set<string>; semanticIdToFilename: Record<string, string> } {
|
|
85
|
+
const mappedFilenames = new Set<string>();
|
|
86
|
+
const semanticIdToFilename: Record<string, string> = {};
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const mappingsContent = require('fs').readFileSync(mappingsPath, 'utf-8');
|
|
90
|
+
const mappings = JSON.parse(mappingsContent);
|
|
91
|
+
Object.entries(mappings).forEach(([semanticId, mapping]) => {
|
|
92
|
+
const filename = typeof mapping === 'string' ? mapping : (mapping as any).filename;
|
|
93
|
+
if (filename) {
|
|
94
|
+
mappedFilenames.add(filename);
|
|
95
|
+
semanticIdToFilename[semanticId] = filename;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
} catch {}
|
|
99
|
+
|
|
100
|
+
return { mappedFilenames, semanticIdToFilename };
|
|
101
|
+
}
|
|
11
102
|
|
|
12
103
|
export async function GET(request: NextRequest) {
|
|
13
104
|
try {
|
|
14
105
|
const { searchParams } = new URL(request.url);
|
|
15
106
|
const page = parseInt(searchParams.get('page') || '1');
|
|
16
|
-
const limit = parseInt(searchParams.get('limit') || '
|
|
107
|
+
const limit = parseInt(searchParams.get('limit') || '1000');
|
|
17
108
|
const search = searchParams.get('search') || '';
|
|
18
109
|
|
|
19
|
-
// Read uploads directory
|
|
20
110
|
let files: string[] = [];
|
|
111
|
+
let totalSize = 0;
|
|
112
|
+
|
|
21
113
|
try {
|
|
22
114
|
files = await readdir(uploadsDir);
|
|
23
115
|
} catch (error) {
|
|
24
|
-
// Directory doesn't exist yet
|
|
25
116
|
return NextResponse.json({
|
|
26
117
|
images: [],
|
|
27
118
|
total: 0,
|
|
28
119
|
page: 1,
|
|
29
120
|
limit,
|
|
121
|
+
stats: {
|
|
122
|
+
totalImages: 0,
|
|
123
|
+
totalSize: 0,
|
|
124
|
+
usedImages: 0,
|
|
125
|
+
availableImages: 0,
|
|
126
|
+
}
|
|
30
127
|
});
|
|
31
128
|
}
|
|
32
129
|
|
|
33
|
-
// Filter image files and get metadata
|
|
34
130
|
const imageExtensions = ['.jpg', '.jpeg', '.png', '.webp', '.gif'];
|
|
35
131
|
const imageFiles = files.filter(file => {
|
|
36
132
|
const ext = path.extname(file).toLowerCase();
|
|
37
133
|
return imageExtensions.includes(ext);
|
|
38
134
|
});
|
|
39
135
|
|
|
40
|
-
// Apply search filter if provided
|
|
41
|
-
const filteredFiles = search
|
|
42
|
-
? imageFiles.filter(file => file.toLowerCase().includes(search.toLowerCase()))
|
|
43
|
-
: imageFiles;
|
|
44
|
-
|
|
45
|
-
// Sort by modification time (newest first)
|
|
46
136
|
const filesWithStats = await Promise.all(
|
|
47
|
-
|
|
137
|
+
imageFiles.map(async (filename) => {
|
|
48
138
|
const filePath = path.join(uploadsDir, filename);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
139
|
+
try {
|
|
140
|
+
const stats = await stat(filePath);
|
|
141
|
+
totalSize += stats.size;
|
|
142
|
+
return {
|
|
143
|
+
filename,
|
|
144
|
+
mtime: stats.mtime,
|
|
145
|
+
size: stats.size,
|
|
146
|
+
};
|
|
147
|
+
} catch {
|
|
148
|
+
return { filename, mtime: new Date(), size: 0 };
|
|
149
|
+
}
|
|
55
150
|
})
|
|
56
151
|
);
|
|
57
152
|
|
|
58
|
-
|
|
153
|
+
// Get mappings
|
|
154
|
+
const { mappedFilenames, semanticIdToFilename } = extractMappings();
|
|
155
|
+
|
|
156
|
+
// Get database references
|
|
157
|
+
const { blogFilenames, userFilenames } = await queryDatabase();
|
|
158
|
+
|
|
159
|
+
// An image is "in use" if it's in mappings OR referenced in database
|
|
160
|
+
// First resolve database references through mappings, then check
|
|
161
|
+
const inUseFilenames = new Set<string>();
|
|
162
|
+
|
|
163
|
+
// Add mapped filenames
|
|
164
|
+
mappedFilenames.forEach(f => inUseFilenames.add(f));
|
|
165
|
+
|
|
166
|
+
// Add database references (check if they resolve through mappings)
|
|
167
|
+
[...blogFilenames, ...userFilenames].forEach(idOrFilename => {
|
|
168
|
+
const resolved = semanticIdToFilename[idOrFilename] || idOrFilename;
|
|
169
|
+
inUseFilenames.add(resolved);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const usedCount = filesWithStats.filter(f => inUseFilenames.has(f.filename)).length;
|
|
173
|
+
|
|
174
|
+
const filteredFiles = search
|
|
175
|
+
? filesWithStats.filter(file => file.filename.toLowerCase().includes(search.toLowerCase()))
|
|
176
|
+
: filesWithStats;
|
|
177
|
+
|
|
178
|
+
filteredFiles.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
59
179
|
|
|
60
|
-
// Paginate
|
|
61
180
|
const start = (page - 1) * limit;
|
|
62
181
|
const end = start + limit;
|
|
63
|
-
const paginatedFiles =
|
|
182
|
+
const paginatedFiles = filteredFiles.slice(start, end);
|
|
64
183
|
|
|
65
|
-
// Build image metadata
|
|
66
184
|
const images = paginatedFiles.map(({ filename, mtime, size }) => {
|
|
67
185
|
const ext = path.extname(filename).toLowerCase();
|
|
68
186
|
const mimeType = ext === '.png' ? 'image/png' :
|
|
@@ -84,6 +202,13 @@ export async function GET(request: NextRequest) {
|
|
|
84
202
|
total: filteredFiles.length,
|
|
85
203
|
page,
|
|
86
204
|
limit,
|
|
205
|
+
stats: {
|
|
206
|
+
totalImages: imageFiles.length,
|
|
207
|
+
totalSize,
|
|
208
|
+
usedImages: usedCount,
|
|
209
|
+
availableImages: imageFiles.length - usedCount,
|
|
210
|
+
},
|
|
211
|
+
mappedImages: Array.from(mappedFilenames),
|
|
87
212
|
});
|
|
88
213
|
} catch (error) {
|
|
89
214
|
console.error('List images error:', error);
|
package/src/api/router.ts
CHANGED
|
@@ -69,6 +69,14 @@ export async function handleImagesApi(
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
// Route: /api/plugin-images/usage (get image usage across plugins)
|
|
73
|
+
else if (route === 'usage') {
|
|
74
|
+
if (method === 'GET') {
|
|
75
|
+
const usageModule = await import('./usage/route');
|
|
76
|
+
return await usageModule.GET(req);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
72
80
|
// Method not allowed
|
|
73
81
|
return NextResponse.json(
|
|
74
82
|
{ error: `Method ${method} not allowed for route: ${route || '/'}` },
|