@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.
Files changed (111) hide show
  1. package/dist/api/list/index.d.ts +18 -0
  2. package/dist/api/list/index.d.ts.map +1 -1
  3. package/dist/api/list/index.js +121 -20
  4. package/dist/api/router.d.ts.map +1 -1
  5. package/dist/api/router.js +7 -0
  6. package/dist/api/usage/route.d.ts +23 -0
  7. package/dist/api/usage/route.d.ts.map +1 -0
  8. package/dist/api/usage/route.js +238 -0
  9. package/dist/components/BackgroundImage.d.ts.map +1 -1
  10. package/dist/components/BackgroundImage.js +5 -17
  11. package/dist/components/GlobalImageEditor.d.ts.map +1 -1
  12. package/dist/components/GlobalImageEditor.js +9 -4
  13. package/dist/components/Image.d.ts +3 -6
  14. package/dist/components/Image.d.ts.map +1 -1
  15. package/dist/components/Image.js +103 -206
  16. package/dist/components/ImageEditor.d.ts.map +1 -1
  17. package/dist/components/ImageEditor.js +21 -125
  18. package/dist/components/ImagePicker.d.ts.map +1 -1
  19. package/dist/components/ImagePicker.js +6 -59
  20. package/dist/utils/fallback.d.ts +9 -4
  21. package/dist/utils/fallback.d.ts.map +1 -1
  22. package/dist/utils/fallback.js +40 -12
  23. package/dist/utils/transforms.d.ts.map +1 -1
  24. package/dist/utils/transforms.js +7 -10
  25. package/dist/views/ImageManager/components/CleanupLibraryModal.d.ts +12 -0
  26. package/dist/views/ImageManager/components/CleanupLibraryModal.d.ts.map +1 -0
  27. package/dist/views/ImageManager/components/CleanupLibraryModal.js +7 -0
  28. package/dist/views/ImageManager/components/DeleteImageModal.d.ts +15 -0
  29. package/dist/views/ImageManager/components/DeleteImageModal.d.ts.map +1 -0
  30. package/dist/views/ImageManager/components/DeleteImageModal.js +8 -0
  31. package/dist/views/ImageManager/components/ImageGrid.d.ts +12 -0
  32. package/dist/views/ImageManager/components/ImageGrid.d.ts.map +1 -0
  33. package/dist/views/ImageManager/components/ImageGrid.js +15 -0
  34. package/dist/views/ImageManager/components/ImageManagerHeader.d.ts +11 -0
  35. package/dist/views/ImageManager/components/ImageManagerHeader.d.ts.map +1 -0
  36. package/dist/views/ImageManager/components/ImageManagerHeader.js +6 -0
  37. package/dist/views/ImageManager/components/ImageManagerStats.d.ts +8 -0
  38. package/dist/views/ImageManager/components/ImageManagerStats.d.ts.map +1 -0
  39. package/dist/views/ImageManager/components/ImageManagerStats.js +6 -0
  40. package/dist/views/ImageManager/components/ImageManagerToolbar.d.ts +9 -0
  41. package/dist/views/ImageManager/components/ImageManagerToolbar.d.ts.map +1 -0
  42. package/dist/views/ImageManager/components/ImageManagerToolbar.js +10 -0
  43. package/dist/views/ImageManager/components/ImageTable.d.ts +13 -0
  44. package/dist/views/ImageManager/components/ImageTable.d.ts.map +1 -0
  45. package/dist/views/ImageManager/components/ImageTable.js +13 -0
  46. package/dist/views/ImageManager/types.d.ts +26 -0
  47. package/dist/views/ImageManager/types.d.ts.map +1 -0
  48. package/dist/views/ImageManager/types.js +1 -0
  49. package/dist/views/ImageManager.d.ts +1 -1
  50. package/dist/views/ImageManager.d.ts.map +1 -1
  51. package/dist/views/ImageManager.js +206 -2
  52. package/package.json +10 -9
  53. package/src/api/list/index.ts +147 -22
  54. package/src/api/router.ts +8 -0
  55. package/src/api/usage/route.ts +294 -0
  56. package/src/components/BackgroundImage.tsx +5 -15
  57. package/src/components/GlobalImageEditor.tsx +9 -4
  58. package/src/components/Image.tsx +128 -268
  59. package/src/components/ImageEditor.tsx +31 -193
  60. package/src/components/ImagePicker.tsx +22 -107
  61. package/src/utils/fallback.ts +46 -13
  62. package/src/utils/transforms.ts +9 -12
  63. package/src/views/ImageManager/components/CleanupLibraryModal.tsx +96 -0
  64. package/src/views/ImageManager/components/DeleteImageModal.tsx +144 -0
  65. package/src/views/ImageManager/components/ImageGrid.tsx +119 -0
  66. package/src/views/ImageManager/components/ImageManagerHeader.tsx +72 -0
  67. package/src/views/ImageManager/components/ImageManagerStats.tsx +60 -0
  68. package/src/views/ImageManager/components/ImageManagerToolbar.tsx +60 -0
  69. package/src/views/ImageManager/components/ImageTable.tsx +120 -0
  70. package/src/views/ImageManager/types.ts +27 -0
  71. package/src/views/ImageManager.tsx +307 -12
  72. package/src/components/BackgroundImage.d.ts +0 -11
  73. package/src/components/BackgroundImage.d.ts.map +0 -1
  74. package/src/components/GlobalImageEditor/config.d.ts +0 -9
  75. package/src/components/GlobalImageEditor/config.d.ts.map +0 -1
  76. package/src/components/GlobalImageEditor/eventHandlers.d.ts +0 -20
  77. package/src/components/GlobalImageEditor/eventHandlers.d.ts.map +0 -1
  78. package/src/components/GlobalImageEditor/imageDetection.d.ts +0 -16
  79. package/src/components/GlobalImageEditor/imageDetection.d.ts.map +0 -1
  80. package/src/components/GlobalImageEditor/imageSetup.d.ts +0 -9
  81. package/src/components/GlobalImageEditor/imageSetup.d.ts.map +0 -1
  82. package/src/components/GlobalImageEditor/saveLogic.d.ts +0 -26
  83. package/src/components/GlobalImageEditor/saveLogic.d.ts.map +0 -1
  84. package/src/components/GlobalImageEditor/stylingDetection.d.ts +0 -9
  85. package/src/components/GlobalImageEditor/stylingDetection.d.ts.map +0 -1
  86. package/src/components/GlobalImageEditor/transformParsing.d.ts +0 -16
  87. package/src/components/GlobalImageEditor/transformParsing.d.ts.map +0 -1
  88. package/src/components/GlobalImageEditor/types.d.ts +0 -36
  89. package/src/components/GlobalImageEditor/types.d.ts.map +0 -1
  90. package/src/components/GlobalImageEditor.d.ts +0 -8
  91. package/src/components/GlobalImageEditor.d.ts.map +0 -1
  92. package/src/components/Image.d.ts +0 -22
  93. package/src/components/Image.d.ts.map +0 -1
  94. package/src/components/ImageBrowserModal.d.ts +0 -13
  95. package/src/components/ImageBrowserModal.d.ts.map +0 -1
  96. package/src/components/ImageEditor.d.ts +0 -27
  97. package/src/components/ImageEditor.d.ts.map +0 -1
  98. package/src/components/ImagePicker.d.ts +0 -3
  99. package/src/components/ImagePicker.d.ts.map +0 -1
  100. package/src/components/ImagesPluginInit.d.ts +0 -24
  101. package/src/components/ImagesPluginInit.d.ts.map +0 -1
  102. package/src/hooks/useImagePicker.d.ts +0 -20
  103. package/src/hooks/useImagePicker.d.ts.map +0 -1
  104. package/src/types/index.d.ts +0 -80
  105. package/src/types/index.d.ts.map +0 -1
  106. package/src/utils/fallback.d.ts +0 -27
  107. package/src/utils/fallback.d.ts.map +0 -1
  108. package/src/utils/transforms.d.ts +0 -26
  109. package/src/utils/transforms.d.ts.map +0 -1
  110. package/src/views/ImageManager.d.ts +0 -10
  111. package/src/views/ImageManager.d.ts.map +0 -1
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Image Manager View
3
- * Main view for managing uploaded images
3
+ * Refactored modular interface for media management
4
4
  */
5
5
  export interface ImageManagerViewProps {
6
6
  siteId: string;
@@ -1 +1 @@
1
- {"version":3,"file":"ImageManager.d.ts","sourceRoot":"","sources":["../../src/views/ImageManager.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,MAAM,WAAW,qBAAqB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,gBAAgB,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,qBAAqB,2CAczE"}
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
- * Main view for managing uploaded images
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
- return (_jsx("div", { className: "min-h-screen bg-dashboard-bg p-8", children: _jsxs("div", { className: "max-w-7xl mx-auto", children: [_jsx("h1", { className: "text-4xl font-black uppercase tracking-tighter text-dashboard-text mb-8", children: "Image Manager" }), _jsx("p", { className: "text-sm text-neutral-600 dark:text-neutral-400 mb-8", children: "This plugin provides image upload and management functionality. Use the ImagePicker component in other plugins to select images." })] }) }));
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.13",
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": "./src/index.ts",
9
- "types": "./src/index.ts",
8
+ "main": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
10
  "exports": {
11
11
  ".": {
12
- "types": "./src/index.tsx",
13
- "import": "./src/index.tsx",
14
- "default": "./src/index.tsx"
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": "./src/index.server.ts",
23
- "import": "./src/index.server.ts",
24
- "default": "./src/index.server.ts"
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"
@@ -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') || '20');
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
- filteredFiles.map(async (filename) => {
137
+ imageFiles.map(async (filename) => {
48
138
  const filePath = path.join(uploadsDir, filename);
49
- const stats = await stat(filePath);
50
- return {
51
- filename,
52
- mtime: stats.mtime,
53
- size: stats.size,
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
- filesWithStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
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 = filesWithStats.slice(start, end);
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 || '/'}` },