@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
@@ -0,0 +1,294 @@
1
+ /**
2
+ * Image Usage API Handler
3
+ * GET /api/plugin-images/usage - Returns usage information for all images
4
+ */
5
+
6
+ import { NextRequest, NextResponse } from 'next/server';
7
+ import { readdir, stat, readFile } from 'fs/promises';
8
+ import path from 'path';
9
+
10
+ const uploadsDir = path.join(process.cwd(), 'data', 'uploads');
11
+ const mappingsPath = path.join(process.cwd(), 'data', 'image-mappings.json');
12
+
13
+ interface ImageUsage {
14
+ imageId: string;
15
+ filename: string;
16
+ usage: Array<{
17
+ plugin: string;
18
+ type: string;
19
+ title: string;
20
+ id: string;
21
+ url?: string;
22
+ }>;
23
+ }
24
+
25
+ async function getMappings(): Promise<Record<string, any>> {
26
+ try {
27
+ const content = await readFile(mappingsPath, 'utf-8');
28
+ return JSON.parse(content);
29
+ } catch {
30
+ return {};
31
+ }
32
+ }
33
+
34
+ async function queryMongoDatabase(): Promise<{
35
+ blogs: Array<{ _id: string; slug?: string; title?: string; image?: { id?: string; src?: string }; contentBlocks?: any[] }>;
36
+ newsletters: Array<{ _id: string; slug?: string; title?: string; content?: any[] }>;
37
+ users: Array<{ _id: string; name?: string; email?: string; image?: string }>;
38
+ }> {
39
+ try {
40
+ const { MongoClient } = await import('mongodb');
41
+ const uri = process.env.DATABASE_URL || process.env.MONGODB_URI;
42
+
43
+ if (!uri) {
44
+ return { blogs: [], newsletters: [], users: [] };
45
+ }
46
+
47
+ const client = new MongoClient(uri);
48
+ await client.connect();
49
+
50
+ const db = client.db();
51
+
52
+ const blogsRaw = await db.collection('blogs')
53
+ .find({}, { projection: { slug: 1, title: 1, image: 1, contentBlocks: 1 } })
54
+ .toArray();
55
+
56
+ const blogs = blogsRaw.map(doc => ({
57
+ _id: doc._id?.toString() || '',
58
+ slug: doc.slug || '',
59
+ title: doc.title || '',
60
+ image: doc.image || {},
61
+ contentBlocks: doc.contentBlocks || [],
62
+ }));
63
+
64
+ const newslettersRaw = await db.collection('newsletters')
65
+ .find({}, { projection: { slug: 1, title: 1, content: 1 } })
66
+ .toArray();
67
+
68
+ const newsletters = newslettersRaw.map(doc => ({
69
+ _id: doc._id?.toString() || '',
70
+ slug: doc.slug || '',
71
+ title: doc.title || '',
72
+ content: doc.content || [],
73
+ }));
74
+
75
+ // Get users with avatars
76
+ const usersRaw = await db.collection('users')
77
+ .find({ image: { $exists: true, $ne: null } })
78
+ .project({ name: 1, email: 1, image: 1 })
79
+ .toArray();
80
+
81
+ const users = usersRaw.map(doc => ({
82
+ _id: doc._id?.toString() || '',
83
+ name: doc.name || '',
84
+ email: doc.email || '',
85
+ image: doc.image || '',
86
+ }));
87
+
88
+ await client.close();
89
+
90
+ return { blogs, newsletters, users };
91
+ } catch (error) {
92
+ console.error('Error querying MongoDB:', error);
93
+ return { blogs: [], newsletters: [], users: [] };
94
+ }
95
+ }
96
+
97
+ function extractImageFilenamesFromContentBlocks(blocks: any[]): string[] {
98
+ const imageFilenames: Set<string> = new Set();
99
+
100
+ function traverse(obj: any) {
101
+ if (!obj || typeof obj !== 'object') return;
102
+
103
+ if (Array.isArray(obj)) {
104
+ obj.forEach(item => traverse(item));
105
+ return;
106
+ }
107
+
108
+ // Check for image object with src or id
109
+ if (obj.src) {
110
+ const filename = obj.src;
111
+ if (filename && !filename.startsWith('http') && !filename.startsWith('data:')) {
112
+ imageFilenames.add(filename);
113
+ }
114
+ }
115
+ // Check for imageId (used by BotanicsAndYou Image block)
116
+ if (obj.imageId && typeof obj.imageId === 'string' && !obj.imageId.startsWith('http')) {
117
+ imageFilenames.add(obj.imageId);
118
+ }
119
+ if (obj.id && typeof obj.id === 'string' && !obj.id.startsWith('http') && !obj.id.startsWith('block-')) {
120
+ // This might be an image ID - but be careful not to catch block IDs
121
+ const ext = path.extname(obj.id);
122
+ if (ext && ['.jpg', '.jpeg', '.png', '.webp', '.gif', '.svg'].includes(ext.toLowerCase())) {
123
+ imageFilenames.add(obj.id);
124
+ }
125
+ }
126
+
127
+ // Check children
128
+ if (obj.children && Array.isArray(obj.children)) {
129
+ obj.children.forEach((child: any) => traverse(child));
130
+ }
131
+ // Check data
132
+ if (obj.data && typeof obj.data === 'object') {
133
+ traverse(obj.data);
134
+ }
135
+ }
136
+
137
+ blocks.forEach(block => traverse(block));
138
+ return Array.from(imageFilenames);
139
+ }
140
+
141
+ function extractImageIdsFromContent(content: any[]): string[] {
142
+ const imageIds: Set<string> = new Set();
143
+
144
+ if (!content || !Array.isArray(content)) {
145
+ return [];
146
+ }
147
+
148
+ for (const block of content) {
149
+ if (block.type === 'image' && block.data?.imageId) {
150
+ imageIds.add(block.data.imageId);
151
+ }
152
+ }
153
+
154
+ return Array.from(imageIds);
155
+ }
156
+
157
+ export async function GET(request: NextRequest) {
158
+ try {
159
+ const { searchParams } = new URL(request.url);
160
+ const filename = searchParams.get('filename');
161
+
162
+ let files: string[] = [];
163
+ try {
164
+ files = await readdir(uploadsDir);
165
+ } catch {
166
+ return NextResponse.json({ images: [] });
167
+ }
168
+
169
+ const imageExtensions = ['.jpg', '.jpeg', '.png', '.webp', '.gif'];
170
+ const imageFiles = files.filter(file => {
171
+ const ext = path.extname(file).toLowerCase();
172
+ return imageExtensions.includes(ext);
173
+ });
174
+
175
+ const mappings = await getMappings();
176
+
177
+ const semanticIdToFilename: Record<string, string> = {};
178
+ Object.entries(mappings).forEach(([semanticId, mapping]) => {
179
+ const mappedFilename = typeof mapping === 'string' ? mapping : mapping.filename;
180
+ if (mappedFilename) {
181
+ semanticIdToFilename[semanticId] = mappedFilename;
182
+ }
183
+ });
184
+
185
+ const { blogs, newsletters, users } = await queryMongoDatabase();
186
+
187
+ const images: ImageUsage[] = imageFiles.map((file) => {
188
+ const usage: ImageUsage['usage'] = [];
189
+
190
+ blogs.forEach((blog) => {
191
+ const imageFilenamesInBlog = new Set<string>();
192
+
193
+ // 1. Featured image at root level (blog.image.id or blog.image.src)
194
+ const featuredImage = blog.image;
195
+ if (featuredImage) {
196
+ const featuredId = featuredImage.id || featuredImage.src;
197
+ if (featuredId) {
198
+ const resolved = semanticIdToFilename[featuredId] || featuredId;
199
+ imageFilenamesInBlog.add(resolved);
200
+
201
+ // Check if this file matches
202
+ if (resolved === file || resolved === path.basename(file, path.extname(file))) {
203
+ usage.push({
204
+ plugin: 'blog',
205
+ type: 'featured-image',
206
+ title: blog.title || blog.slug || 'Untitled',
207
+ id: blog.slug || blog._id,
208
+ url: `/dashboard/blog/editor/${blog.slug || blog._id}`,
209
+ });
210
+ }
211
+ }
212
+ }
213
+
214
+ // 2. Images in contentBlocks
215
+ const contentBlockImages = extractImageFilenamesFromContentBlocks(blog.contentBlocks || []);
216
+ contentBlockImages.forEach(imgId => {
217
+ const resolved = semanticIdToFilename[imgId] || imgId;
218
+ imageFilenamesInBlog.add(resolved);
219
+
220
+ if (resolved === file || resolved === path.basename(file, path.extname(file))) {
221
+ // Avoid duplicate entries for the same blog
222
+ const existing = usage.find(u => u.id === (blog.slug || blog._id) && u.plugin === 'blog');
223
+ if (!existing) {
224
+ usage.push({
225
+ plugin: 'blog',
226
+ type: 'content-image',
227
+ title: blog.title || blog.slug || 'Untitled',
228
+ id: blog.slug || blog._id,
229
+ url: `/dashboard/blog/editor/${blog.slug || blog._id}`,
230
+ });
231
+ }
232
+ }
233
+ });
234
+ });
235
+
236
+ newsletters.forEach((newsletter) => {
237
+ const imageIds = extractImageIdsFromContent(newsletter.content || []);
238
+ imageIds.forEach(imageId => {
239
+ const resolvedFilename = semanticIdToFilename[imageId] || imageId;
240
+ if (resolvedFilename === file || resolvedFilename === path.basename(file, path.extname(file))) {
241
+ const existing = usage.find(u => u.id === (newsletter.slug || newsletter._id) && u.plugin === 'newsletter');
242
+ if (!existing) {
243
+ usage.push({
244
+ plugin: 'newsletter',
245
+ type: 'image-block',
246
+ title: newsletter.title || newsletter.slug || 'Untitled',
247
+ id: newsletter.slug || newsletter._id,
248
+ url: `/dashboard/newsletter/editor/${newsletter.slug || newsletter._id}`,
249
+ });
250
+ }
251
+ }
252
+ });
253
+ });
254
+
255
+ // Check user avatars
256
+ users.forEach((user) => {
257
+ if (user.image && user.image.includes('/api/uploads/')) {
258
+ const avatarFilename = user.image.split('/api/uploads/')[1];
259
+ if (avatarFilename && (avatarFilename === file || avatarFilename === path.basename(file, path.extname(file)))) {
260
+ const existing = usage.find(u => u.id === user._id && u.plugin === 'users');
261
+ if (!existing) {
262
+ usage.push({
263
+ plugin: 'users',
264
+ type: 'avatar',
265
+ title: user.name || user.email || 'Unknown User',
266
+ id: user._id,
267
+ url: `/dashboard/users`,
268
+ });
269
+ }
270
+ }
271
+ }
272
+ });
273
+
274
+ return {
275
+ imageId: file,
276
+ filename: file,
277
+ usage,
278
+ };
279
+ });
280
+
281
+ if (filename) {
282
+ const filtered = images.filter(img => img.filename === filename || img.imageId === filename);
283
+ return NextResponse.json({ images: filtered });
284
+ }
285
+
286
+ return NextResponse.json({ images });
287
+ } catch (error) {
288
+ console.error('Get image usage error:', error);
289
+ return NextResponse.json(
290
+ { error: 'Failed to get image usage' },
291
+ { status: 500 }
292
+ );
293
+ }
294
+ }
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import React, { useState, useEffect } from 'react';
4
- import { Image } from './Image';
4
+ import { Image, checkAuthOnce } from './Image';
5
5
  import { Edit2 } from 'lucide-react';
6
6
 
7
7
  export interface BackgroundImageProps {
@@ -25,20 +25,10 @@ export function BackgroundImage({
25
25
  const [isLoading, setIsLoading] = useState(true);
26
26
 
27
27
  useEffect(() => {
28
- const checkUser = async () => {
29
- try {
30
- const res = await fetch('/api/me');
31
- const data = await res.json();
32
- if (data.loggedIn && ['admin', 'dev'].includes(data.user?.role)) {
33
- setIsAdmin(true);
34
- }
35
- } catch (error) {
36
- console.error('Auth error:', error);
37
- } finally {
38
- setIsLoading(false);
39
- }
40
- };
41
- checkUser();
28
+ checkAuthOnce().then(isAuth => {
29
+ setIsAdmin(isAuth);
30
+ setIsLoading(false);
31
+ });
42
32
  }, []);
43
33
 
44
34
  const handleEditClick = (e: React.MouseEvent) => {
@@ -17,6 +17,7 @@ import { parseImageData } from './GlobalImageEditor/imageDetection';
17
17
  import { setupImageHandlers } from './GlobalImageEditor/imageSetup';
18
18
  import { saveTransformToAPI, flushPendingSave, getFilename, normalizePosition } from './GlobalImageEditor/saveLogic';
19
19
  import { handleImageChange, handleBrightnessChange, handleBlurChange } from './GlobalImageEditor/eventHandlers';
20
+ import { checkAuthOnce } from './Image';
20
21
 
21
22
  export function GlobalImageEditor() {
22
23
  // Configuration
@@ -36,10 +37,14 @@ export function GlobalImageEditor() {
36
37
  useEffect(() => {
37
38
  const checkUser = async () => {
38
39
  try {
39
- const res = await fetch('/api/me');
40
- const data = await res.json();
41
- if (data.loggedIn && (data.user?.role === 'admin' || data.user?.role === 'dev')) {
42
- setUserRole(data.user.role);
40
+ const isAuth = await checkAuthOnce();
41
+ if (isAuth) {
42
+ // Need to fetch again to get user role since checkAuthOnce only returns boolean
43
+ const res = await fetch('/api/me');
44
+ const data = await res.json();
45
+ if (data.user?.role === 'admin' || data.user?.role === 'dev') {
46
+ setUserRole(data.user.role);
47
+ }
43
48
  }
44
49
  } catch (error) {
45
50
  console.error('Failed to check user role:', error);