@jhits/plugin-images 0.0.12 → 0.0.14
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 +2 -0
- package/dist/components/Image.d.ts.map +1 -1
- package/dist/components/Image.js +22 -9
- package/dist/views/ImageManager.d.ts +1 -1
- package/dist/views/ImageManager.d.ts.map +1 -1
- package/dist/views/ImageManager.js +231 -3
- package/package.json +2 -2
- 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 +24 -9
- package/src/views/ImageManager.tsx +775 -12
package/dist/api/list/index.d.ts
CHANGED
|
@@ -4,6 +4,17 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { NextRequest, NextResponse } from 'next/server';
|
|
6
6
|
export declare function GET(request: NextRequest): Promise<NextResponse<{
|
|
7
|
+
images: never[];
|
|
8
|
+
total: number;
|
|
9
|
+
page: number;
|
|
10
|
+
limit: number;
|
|
11
|
+
stats: {
|
|
12
|
+
totalImages: number;
|
|
13
|
+
totalSize: number;
|
|
14
|
+
usedImages: number;
|
|
15
|
+
availableImages: number;
|
|
16
|
+
};
|
|
17
|
+
}> | NextResponse<{
|
|
7
18
|
images: {
|
|
8
19
|
id: string;
|
|
9
20
|
filename: string;
|
|
@@ -15,6 +26,13 @@ export declare function GET(request: NextRequest): Promise<NextResponse<{
|
|
|
15
26
|
total: number;
|
|
16
27
|
page: number;
|
|
17
28
|
limit: number;
|
|
29
|
+
stats: {
|
|
30
|
+
totalImages: number;
|
|
31
|
+
totalSize: number;
|
|
32
|
+
usedImages: number;
|
|
33
|
+
availableImages: number;
|
|
34
|
+
};
|
|
35
|
+
mappedImages: string[];
|
|
18
36
|
}> | NextResponse<{
|
|
19
37
|
error: string;
|
|
20
38
|
}>>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/api/list/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/api/list/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAiGxD,wBAAsB,GAAG,CAAC,OAAO,EAAE,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAqH7C"}
|
package/dist/api/list/index.js
CHANGED
|
@@ -6,52 +6,146 @@ import { NextResponse } from 'next/server';
|
|
|
6
6
|
import { readdir, stat } from 'fs/promises';
|
|
7
7
|
import path from 'path';
|
|
8
8
|
const uploadsDir = path.join(process.cwd(), 'data', 'uploads');
|
|
9
|
+
const mappingsPath = path.join(process.cwd(), 'data', 'image-mappings.json');
|
|
10
|
+
async function queryDatabase() {
|
|
11
|
+
const blogFilenames = new Set();
|
|
12
|
+
const userFilenames = new Set();
|
|
13
|
+
try {
|
|
14
|
+
const { MongoClient } = await import('mongodb');
|
|
15
|
+
const uri = process.env.DATABASE_URL || process.env.MONGODB_URI;
|
|
16
|
+
if (!uri)
|
|
17
|
+
return { blogFilenames, userFilenames };
|
|
18
|
+
const client = new MongoClient(uri);
|
|
19
|
+
await client.connect();
|
|
20
|
+
const db = client.db();
|
|
21
|
+
// Get blogs
|
|
22
|
+
const blogs = await db.collection('blogs').find({}).project({ image: 1, contentBlocks: 1 }).toArray();
|
|
23
|
+
for (const blog of blogs) {
|
|
24
|
+
// Featured image
|
|
25
|
+
if (blog.image?.id)
|
|
26
|
+
blogFilenames.add(blog.image.id);
|
|
27
|
+
if (blog.image?.src)
|
|
28
|
+
blogFilenames.add(blog.image.src);
|
|
29
|
+
// Content blocks - recursively find image.src
|
|
30
|
+
if (blog.contentBlocks) {
|
|
31
|
+
const findImages = (obj) => {
|
|
32
|
+
if (!obj || typeof obj !== 'object')
|
|
33
|
+
return;
|
|
34
|
+
if (Array.isArray(obj)) {
|
|
35
|
+
obj.forEach(findImages);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (obj.src && typeof obj.src === 'string' && !obj.src.startsWith('http')) {
|
|
39
|
+
blogFilenames.add(obj.src);
|
|
40
|
+
}
|
|
41
|
+
if (obj.children)
|
|
42
|
+
findImages(obj.children);
|
|
43
|
+
if (obj.data)
|
|
44
|
+
findImages(obj.data);
|
|
45
|
+
};
|
|
46
|
+
findImages(blog.contentBlocks);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Get users with avatars
|
|
50
|
+
const users = await db.collection('users').find({ image: { $exists: true, $ne: null } }).project({ image: 1 }).toArray();
|
|
51
|
+
for (const user of users) {
|
|
52
|
+
if (user.image && user.image.includes('/api/uploads/')) {
|
|
53
|
+
const filename = user.image.split('/api/uploads/')[1];
|
|
54
|
+
if (filename)
|
|
55
|
+
userFilenames.add(filename);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
await client.close();
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
console.error('Error querying database for list:', error);
|
|
62
|
+
}
|
|
63
|
+
return { blogFilenames, userFilenames };
|
|
64
|
+
}
|
|
65
|
+
function extractMappings() {
|
|
66
|
+
const mappedFilenames = new Set();
|
|
67
|
+
const semanticIdToFilename = {};
|
|
68
|
+
try {
|
|
69
|
+
const mappingsContent = require('fs').readFileSync(mappingsPath, 'utf-8');
|
|
70
|
+
const mappings = JSON.parse(mappingsContent);
|
|
71
|
+
Object.entries(mappings).forEach(([semanticId, mapping]) => {
|
|
72
|
+
const filename = typeof mapping === 'string' ? mapping : mapping.filename;
|
|
73
|
+
if (filename) {
|
|
74
|
+
mappedFilenames.add(filename);
|
|
75
|
+
semanticIdToFilename[semanticId] = filename;
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
catch { }
|
|
80
|
+
return { mappedFilenames, semanticIdToFilename };
|
|
81
|
+
}
|
|
9
82
|
export async function GET(request) {
|
|
10
83
|
try {
|
|
11
84
|
const { searchParams } = new URL(request.url);
|
|
12
85
|
const page = parseInt(searchParams.get('page') || '1');
|
|
13
|
-
const limit = parseInt(searchParams.get('limit') || '
|
|
86
|
+
const limit = parseInt(searchParams.get('limit') || '1000');
|
|
14
87
|
const search = searchParams.get('search') || '';
|
|
15
|
-
// Read uploads directory
|
|
16
88
|
let files = [];
|
|
89
|
+
let totalSize = 0;
|
|
17
90
|
try {
|
|
18
91
|
files = await readdir(uploadsDir);
|
|
19
92
|
}
|
|
20
93
|
catch (error) {
|
|
21
|
-
// Directory doesn't exist yet
|
|
22
94
|
return NextResponse.json({
|
|
23
95
|
images: [],
|
|
24
96
|
total: 0,
|
|
25
97
|
page: 1,
|
|
26
98
|
limit,
|
|
99
|
+
stats: {
|
|
100
|
+
totalImages: 0,
|
|
101
|
+
totalSize: 0,
|
|
102
|
+
usedImages: 0,
|
|
103
|
+
availableImages: 0,
|
|
104
|
+
}
|
|
27
105
|
});
|
|
28
106
|
}
|
|
29
|
-
// Filter image files and get metadata
|
|
30
107
|
const imageExtensions = ['.jpg', '.jpeg', '.png', '.webp', '.gif'];
|
|
31
108
|
const imageFiles = files.filter(file => {
|
|
32
109
|
const ext = path.extname(file).toLowerCase();
|
|
33
110
|
return imageExtensions.includes(ext);
|
|
34
111
|
});
|
|
35
|
-
|
|
36
|
-
const filteredFiles = search
|
|
37
|
-
? imageFiles.filter(file => file.toLowerCase().includes(search.toLowerCase()))
|
|
38
|
-
: imageFiles;
|
|
39
|
-
// Sort by modification time (newest first)
|
|
40
|
-
const filesWithStats = await Promise.all(filteredFiles.map(async (filename) => {
|
|
112
|
+
const filesWithStats = await Promise.all(imageFiles.map(async (filename) => {
|
|
41
113
|
const filePath = path.join(uploadsDir, filename);
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
114
|
+
try {
|
|
115
|
+
const stats = await stat(filePath);
|
|
116
|
+
totalSize += stats.size;
|
|
117
|
+
return {
|
|
118
|
+
filename,
|
|
119
|
+
mtime: stats.mtime,
|
|
120
|
+
size: stats.size,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return { filename, mtime: new Date(), size: 0 };
|
|
125
|
+
}
|
|
48
126
|
}));
|
|
49
|
-
|
|
50
|
-
|
|
127
|
+
// Get mappings
|
|
128
|
+
const { mappedFilenames, semanticIdToFilename } = extractMappings();
|
|
129
|
+
// Get database references
|
|
130
|
+
const { blogFilenames, userFilenames } = await queryDatabase();
|
|
131
|
+
// An image is "in use" if it's in mappings OR referenced in database
|
|
132
|
+
// First resolve database references through mappings, then check
|
|
133
|
+
const inUseFilenames = new Set();
|
|
134
|
+
// Add mapped filenames
|
|
135
|
+
mappedFilenames.forEach(f => inUseFilenames.add(f));
|
|
136
|
+
// Add database references (check if they resolve through mappings)
|
|
137
|
+
[...blogFilenames, ...userFilenames].forEach(idOrFilename => {
|
|
138
|
+
const resolved = semanticIdToFilename[idOrFilename] || idOrFilename;
|
|
139
|
+
inUseFilenames.add(resolved);
|
|
140
|
+
});
|
|
141
|
+
const usedCount = filesWithStats.filter(f => inUseFilenames.has(f.filename)).length;
|
|
142
|
+
const filteredFiles = search
|
|
143
|
+
? filesWithStats.filter(file => file.filename.toLowerCase().includes(search.toLowerCase()))
|
|
144
|
+
: filesWithStats;
|
|
145
|
+
filteredFiles.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
51
146
|
const start = (page - 1) * limit;
|
|
52
147
|
const end = start + limit;
|
|
53
|
-
const paginatedFiles =
|
|
54
|
-
// Build image metadata
|
|
148
|
+
const paginatedFiles = filteredFiles.slice(start, end);
|
|
55
149
|
const images = paginatedFiles.map(({ filename, mtime, size }) => {
|
|
56
150
|
const ext = path.extname(filename).toLowerCase();
|
|
57
151
|
const mimeType = ext === '.png' ? 'image/png' :
|
|
@@ -71,6 +165,13 @@ export async function GET(request) {
|
|
|
71
165
|
total: filteredFiles.length,
|
|
72
166
|
page,
|
|
73
167
|
limit,
|
|
168
|
+
stats: {
|
|
169
|
+
totalImages: imageFiles.length,
|
|
170
|
+
totalSize,
|
|
171
|
+
usedImages: usedCount,
|
|
172
|
+
availableImages: imageFiles.length - usedCount,
|
|
173
|
+
},
|
|
174
|
+
mappedImages: Array.from(mappedFilenames),
|
|
74
175
|
});
|
|
75
176
|
}
|
|
76
177
|
catch (error) {
|
package/dist/api/router.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../src/api/router.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAExD;;;GAGG;AACH,wBAAsB,eAAe,CACjC,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE,MAAM,EAAE,GACf,OAAO,CAAC,YAAY,CAAC,
|
|
1
|
+
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../src/api/router.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAExD;;;GAGG;AACH,wBAAsB,eAAe,CACjC,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE,MAAM,EAAE,GACf,OAAO,CAAC,YAAY,CAAC,CAwEvB"}
|
package/dist/api/router.js
CHANGED
|
@@ -57,6 +57,13 @@ export async function handleImagesApi(req, path) {
|
|
|
57
57
|
return await fallbackModule.GET(req);
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
|
+
// Route: /api/plugin-images/usage (get image usage across plugins)
|
|
61
|
+
else if (route === 'usage') {
|
|
62
|
+
if (method === 'GET') {
|
|
63
|
+
const usageModule = await import('./usage/route');
|
|
64
|
+
return await usageModule.GET(req);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
60
67
|
// Method not allowed
|
|
61
68
|
return NextResponse.json({ error: `Method ${method} not allowed for route: ${route || '/'}` }, { status: 405 });
|
|
62
69
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Usage API Handler
|
|
3
|
+
* GET /api/plugin-images/usage - Returns usage information for all images
|
|
4
|
+
*/
|
|
5
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
6
|
+
interface ImageUsage {
|
|
7
|
+
imageId: string;
|
|
8
|
+
filename: string;
|
|
9
|
+
usage: Array<{
|
|
10
|
+
plugin: string;
|
|
11
|
+
type: string;
|
|
12
|
+
title: string;
|
|
13
|
+
id: string;
|
|
14
|
+
url?: string;
|
|
15
|
+
}>;
|
|
16
|
+
}
|
|
17
|
+
export declare function GET(request: NextRequest): Promise<NextResponse<{
|
|
18
|
+
images: ImageUsage[];
|
|
19
|
+
}> | NextResponse<{
|
|
20
|
+
error: string;
|
|
21
|
+
}>>;
|
|
22
|
+
export {};
|
|
23
|
+
//# sourceMappingURL=route.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../src/api/usage/route.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAOxD,UAAU,UAAU;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,KAAK,CAAC;QACT,MAAM,EAAE,MAAM,CAAC;QACf,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;QACd,EAAE,EAAE,MAAM,CAAC;QACX,GAAG,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC,CAAC;CACN;AAsID,wBAAsB,GAAG,CAAC,OAAO,EAAE,WAAW;;;;IAyI7C"}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Usage API Handler
|
|
3
|
+
* GET /api/plugin-images/usage - Returns usage information for all images
|
|
4
|
+
*/
|
|
5
|
+
import { NextResponse } from 'next/server';
|
|
6
|
+
import { readdir, readFile } from 'fs/promises';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
const uploadsDir = path.join(process.cwd(), 'data', 'uploads');
|
|
9
|
+
const mappingsPath = path.join(process.cwd(), 'data', 'image-mappings.json');
|
|
10
|
+
async function getMappings() {
|
|
11
|
+
try {
|
|
12
|
+
const content = await readFile(mappingsPath, 'utf-8');
|
|
13
|
+
return JSON.parse(content);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
async function queryMongoDatabase() {
|
|
20
|
+
try {
|
|
21
|
+
const { MongoClient } = await import('mongodb');
|
|
22
|
+
const uri = process.env.DATABASE_URL || process.env.MONGODB_URI;
|
|
23
|
+
if (!uri) {
|
|
24
|
+
return { blogs: [], newsletters: [], users: [] };
|
|
25
|
+
}
|
|
26
|
+
const client = new MongoClient(uri);
|
|
27
|
+
await client.connect();
|
|
28
|
+
const db = client.db();
|
|
29
|
+
const blogsRaw = await db.collection('blogs')
|
|
30
|
+
.find({}, { projection: { slug: 1, title: 1, image: 1, contentBlocks: 1 } })
|
|
31
|
+
.toArray();
|
|
32
|
+
const blogs = blogsRaw.map(doc => ({
|
|
33
|
+
_id: doc._id?.toString() || '',
|
|
34
|
+
slug: doc.slug || '',
|
|
35
|
+
title: doc.title || '',
|
|
36
|
+
image: doc.image || {},
|
|
37
|
+
contentBlocks: doc.contentBlocks || [],
|
|
38
|
+
}));
|
|
39
|
+
const newslettersRaw = await db.collection('newsletters')
|
|
40
|
+
.find({}, { projection: { slug: 1, title: 1, content: 1 } })
|
|
41
|
+
.toArray();
|
|
42
|
+
const newsletters = newslettersRaw.map(doc => ({
|
|
43
|
+
_id: doc._id?.toString() || '',
|
|
44
|
+
slug: doc.slug || '',
|
|
45
|
+
title: doc.title || '',
|
|
46
|
+
content: doc.content || [],
|
|
47
|
+
}));
|
|
48
|
+
// Get users with avatars
|
|
49
|
+
const usersRaw = await db.collection('users')
|
|
50
|
+
.find({ image: { $exists: true, $ne: null } })
|
|
51
|
+
.project({ name: 1, email: 1, image: 1 })
|
|
52
|
+
.toArray();
|
|
53
|
+
const users = usersRaw.map(doc => ({
|
|
54
|
+
_id: doc._id?.toString() || '',
|
|
55
|
+
name: doc.name || '',
|
|
56
|
+
email: doc.email || '',
|
|
57
|
+
image: doc.image || '',
|
|
58
|
+
}));
|
|
59
|
+
await client.close();
|
|
60
|
+
return { blogs, newsletters, users };
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
console.error('Error querying MongoDB:', error);
|
|
64
|
+
return { blogs: [], newsletters: [], users: [] };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function extractImageFilenamesFromContentBlocks(blocks) {
|
|
68
|
+
const imageFilenames = new Set();
|
|
69
|
+
function traverse(obj) {
|
|
70
|
+
if (!obj || typeof obj !== 'object')
|
|
71
|
+
return;
|
|
72
|
+
if (Array.isArray(obj)) {
|
|
73
|
+
obj.forEach(item => traverse(item));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
// Check for image object with src or id
|
|
77
|
+
if (obj.src) {
|
|
78
|
+
const filename = obj.src;
|
|
79
|
+
if (filename && !filename.startsWith('http') && !filename.startsWith('data:')) {
|
|
80
|
+
imageFilenames.add(filename);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Check for imageId (used by BotanicsAndYou Image block)
|
|
84
|
+
if (obj.imageId && typeof obj.imageId === 'string' && !obj.imageId.startsWith('http')) {
|
|
85
|
+
imageFilenames.add(obj.imageId);
|
|
86
|
+
}
|
|
87
|
+
if (obj.id && typeof obj.id === 'string' && !obj.id.startsWith('http') && !obj.id.startsWith('block-')) {
|
|
88
|
+
// This might be an image ID - but be careful not to catch block IDs
|
|
89
|
+
const ext = path.extname(obj.id);
|
|
90
|
+
if (ext && ['.jpg', '.jpeg', '.png', '.webp', '.gif', '.svg'].includes(ext.toLowerCase())) {
|
|
91
|
+
imageFilenames.add(obj.id);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Check children
|
|
95
|
+
if (obj.children && Array.isArray(obj.children)) {
|
|
96
|
+
obj.children.forEach((child) => traverse(child));
|
|
97
|
+
}
|
|
98
|
+
// Check data
|
|
99
|
+
if (obj.data && typeof obj.data === 'object') {
|
|
100
|
+
traverse(obj.data);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
blocks.forEach(block => traverse(block));
|
|
104
|
+
return Array.from(imageFilenames);
|
|
105
|
+
}
|
|
106
|
+
function extractImageIdsFromContent(content) {
|
|
107
|
+
const imageIds = new Set();
|
|
108
|
+
if (!content || !Array.isArray(content)) {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
for (const block of content) {
|
|
112
|
+
if (block.type === 'image' && block.data?.imageId) {
|
|
113
|
+
imageIds.add(block.data.imageId);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return Array.from(imageIds);
|
|
117
|
+
}
|
|
118
|
+
export async function GET(request) {
|
|
119
|
+
try {
|
|
120
|
+
const { searchParams } = new URL(request.url);
|
|
121
|
+
const filename = searchParams.get('filename');
|
|
122
|
+
let files = [];
|
|
123
|
+
try {
|
|
124
|
+
files = await readdir(uploadsDir);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return NextResponse.json({ images: [] });
|
|
128
|
+
}
|
|
129
|
+
const imageExtensions = ['.jpg', '.jpeg', '.png', '.webp', '.gif'];
|
|
130
|
+
const imageFiles = files.filter(file => {
|
|
131
|
+
const ext = path.extname(file).toLowerCase();
|
|
132
|
+
return imageExtensions.includes(ext);
|
|
133
|
+
});
|
|
134
|
+
const mappings = await getMappings();
|
|
135
|
+
const semanticIdToFilename = {};
|
|
136
|
+
Object.entries(mappings).forEach(([semanticId, mapping]) => {
|
|
137
|
+
const mappedFilename = typeof mapping === 'string' ? mapping : mapping.filename;
|
|
138
|
+
if (mappedFilename) {
|
|
139
|
+
semanticIdToFilename[semanticId] = mappedFilename;
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
const { blogs, newsletters, users } = await queryMongoDatabase();
|
|
143
|
+
const images = imageFiles.map((file) => {
|
|
144
|
+
const usage = [];
|
|
145
|
+
blogs.forEach((blog) => {
|
|
146
|
+
const imageFilenamesInBlog = new Set();
|
|
147
|
+
// 1. Featured image at root level (blog.image.id or blog.image.src)
|
|
148
|
+
const featuredImage = blog.image;
|
|
149
|
+
if (featuredImage) {
|
|
150
|
+
const featuredId = featuredImage.id || featuredImage.src;
|
|
151
|
+
if (featuredId) {
|
|
152
|
+
const resolved = semanticIdToFilename[featuredId] || featuredId;
|
|
153
|
+
imageFilenamesInBlog.add(resolved);
|
|
154
|
+
// Check if this file matches
|
|
155
|
+
if (resolved === file || resolved === path.basename(file, path.extname(file))) {
|
|
156
|
+
usage.push({
|
|
157
|
+
plugin: 'blog',
|
|
158
|
+
type: 'featured-image',
|
|
159
|
+
title: blog.title || blog.slug || 'Untitled',
|
|
160
|
+
id: blog.slug || blog._id,
|
|
161
|
+
url: `/dashboard/blog/editor/${blog.slug || blog._id}`,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// 2. Images in contentBlocks
|
|
167
|
+
const contentBlockImages = extractImageFilenamesFromContentBlocks(blog.contentBlocks || []);
|
|
168
|
+
contentBlockImages.forEach(imgId => {
|
|
169
|
+
const resolved = semanticIdToFilename[imgId] || imgId;
|
|
170
|
+
imageFilenamesInBlog.add(resolved);
|
|
171
|
+
if (resolved === file || resolved === path.basename(file, path.extname(file))) {
|
|
172
|
+
// Avoid duplicate entries for the same blog
|
|
173
|
+
const existing = usage.find(u => u.id === (blog.slug || blog._id) && u.plugin === 'blog');
|
|
174
|
+
if (!existing) {
|
|
175
|
+
usage.push({
|
|
176
|
+
plugin: 'blog',
|
|
177
|
+
type: 'content-image',
|
|
178
|
+
title: blog.title || blog.slug || 'Untitled',
|
|
179
|
+
id: blog.slug || blog._id,
|
|
180
|
+
url: `/dashboard/blog/editor/${blog.slug || blog._id}`,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
newsletters.forEach((newsletter) => {
|
|
187
|
+
const imageIds = extractImageIdsFromContent(newsletter.content || []);
|
|
188
|
+
imageIds.forEach(imageId => {
|
|
189
|
+
const resolvedFilename = semanticIdToFilename[imageId] || imageId;
|
|
190
|
+
if (resolvedFilename === file || resolvedFilename === path.basename(file, path.extname(file))) {
|
|
191
|
+
const existing = usage.find(u => u.id === (newsletter.slug || newsletter._id) && u.plugin === 'newsletter');
|
|
192
|
+
if (!existing) {
|
|
193
|
+
usage.push({
|
|
194
|
+
plugin: 'newsletter',
|
|
195
|
+
type: 'image-block',
|
|
196
|
+
title: newsletter.title || newsletter.slug || 'Untitled',
|
|
197
|
+
id: newsletter.slug || newsletter._id,
|
|
198
|
+
url: `/dashboard/newsletter/editor/${newsletter.slug || newsletter._id}`,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
// Check user avatars
|
|
205
|
+
users.forEach((user) => {
|
|
206
|
+
if (user.image && user.image.includes('/api/uploads/')) {
|
|
207
|
+
const avatarFilename = user.image.split('/api/uploads/')[1];
|
|
208
|
+
if (avatarFilename && (avatarFilename === file || avatarFilename === path.basename(file, path.extname(file)))) {
|
|
209
|
+
const existing = usage.find(u => u.id === user._id && u.plugin === 'users');
|
|
210
|
+
if (!existing) {
|
|
211
|
+
usage.push({
|
|
212
|
+
plugin: 'users',
|
|
213
|
+
type: 'avatar',
|
|
214
|
+
title: user.name || user.email || 'Unknown User',
|
|
215
|
+
id: user._id,
|
|
216
|
+
url: `/dashboard/users`,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
return {
|
|
223
|
+
imageId: file,
|
|
224
|
+
filename: file,
|
|
225
|
+
usage,
|
|
226
|
+
};
|
|
227
|
+
});
|
|
228
|
+
if (filename) {
|
|
229
|
+
const filtered = images.filter(img => img.filename === filename || img.imageId === filename);
|
|
230
|
+
return NextResponse.json({ images: filtered });
|
|
231
|
+
}
|
|
232
|
+
return NextResponse.json({ images });
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
console.error('Get image usage error:', error);
|
|
236
|
+
return NextResponse.json({ error: 'Failed to get image usage' }, { status: 500 });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BackgroundImage.d.ts","sourceRoot":"","sources":["../../src/components/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,
|
|
1
|
+
{"version":3,"file":"BackgroundImage.d.ts","sourceRoot":"","sources":["../../src/components/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,2CA2DtB"}
|
|
@@ -1,28 +1,16 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useState, useEffect } from 'react';
|
|
4
|
-
import { Image } from './Image';
|
|
4
|
+
import { Image, checkAuthOnce } from './Image';
|
|
5
5
|
import { Edit2 } from 'lucide-react';
|
|
6
6
|
export function BackgroundImage({ id, className = '', style = {}, children, backgroundSize = 'cover', backgroundPosition = 'center', }) {
|
|
7
7
|
const [isAdmin, setIsAdmin] = useState(false);
|
|
8
8
|
const [isLoading, setIsLoading] = useState(true);
|
|
9
9
|
useEffect(() => {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (data.loggedIn && ['admin', 'dev'].includes(data.user?.role)) {
|
|
15
|
-
setIsAdmin(true);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
catch (error) {
|
|
19
|
-
console.error('Auth error:', error);
|
|
20
|
-
}
|
|
21
|
-
finally {
|
|
22
|
-
setIsLoading(false);
|
|
23
|
-
}
|
|
24
|
-
};
|
|
25
|
-
checkUser();
|
|
10
|
+
checkAuthOnce().then(isAuth => {
|
|
11
|
+
setIsAdmin(isAuth);
|
|
12
|
+
setIsLoading(false);
|
|
13
|
+
});
|
|
26
14
|
}, []);
|
|
27
15
|
const handleEditClick = (e) => {
|
|
28
16
|
e.preventDefault();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GlobalImageEditor.d.ts","sourceRoot":"","sources":["../../src/components/GlobalImageEditor.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
1
|
+
{"version":3,"file":"GlobalImageEditor.d.ts","sourceRoot":"","sources":["../../src/components/GlobalImageEditor.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAgBH,wBAAgB,iBAAiB,mDAsThC"}
|
|
@@ -14,6 +14,7 @@ import { parseImageData } from './GlobalImageEditor/imageDetection';
|
|
|
14
14
|
import { setupImageHandlers } from './GlobalImageEditor/imageSetup';
|
|
15
15
|
import { saveTransformToAPI, flushPendingSave, getFilename, normalizePosition } from './GlobalImageEditor/saveLogic';
|
|
16
16
|
import { handleImageChange, handleBrightnessChange, handleBlurChange } from './GlobalImageEditor/eventHandlers';
|
|
17
|
+
import { checkAuthOnce } from './Image';
|
|
17
18
|
export function GlobalImageEditor() {
|
|
18
19
|
// Configuration
|
|
19
20
|
const config = useMemo(() => getPluginConfig(), []);
|
|
@@ -29,10 +30,14 @@ export function GlobalImageEditor() {
|
|
|
29
30
|
useEffect(() => {
|
|
30
31
|
const checkUser = async () => {
|
|
31
32
|
try {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
const isAuth = await checkAuthOnce();
|
|
34
|
+
if (isAuth) {
|
|
35
|
+
// Need to fetch again to get user role since checkAuthOnce only returns boolean
|
|
36
|
+
const res = await fetch('/api/me');
|
|
37
|
+
const data = await res.json();
|
|
38
|
+
if (data.user?.role === 'admin' || data.user?.role === 'dev') {
|
|
39
|
+
setUserRole(data.user.role);
|
|
40
|
+
}
|
|
36
41
|
}
|
|
37
42
|
}
|
|
38
43
|
catch (error) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Image.d.ts","sourceRoot":"","sources":["../../src/components/Image.tsx"],"names":[],"mappings":"AAGA,OAAO,KAA4D,MAAM,OAAO,CAAC;AAKjF,MAAM,WAAW,gBAAgB;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,SAAS,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,YAAY,CAAC;IACjE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAC5B,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,KAAK,CAAC,EAClB,EAAE,EACF,GAAG,EACH,KAAK,EACL,MAAM,EACN,SAAc,EACd,IAAY,EACZ,KAAK,EACL,QAAgB,EAChB,SAAmB,EACnB,cAAyB,EACzB,KAAK,EACL,QAAe,EACf,GAAG,KAAK,EACX,EAAE,gBAAgB,
|
|
1
|
+
{"version":3,"file":"Image.d.ts","sourceRoot":"","sources":["../../src/components/Image.tsx"],"names":[],"mappings":"AAGA,OAAO,KAA4D,MAAM,OAAO,CAAC;AAKjF,eAAO,IAAI,SAAS,EAAE,OAAO,GAAG,IAAW,CAAC;AAG5C,wBAAsB,aAAa,IAAI,OAAO,CAAC,OAAO,CAAC,CAiBtD;AAED,MAAM,WAAW,gBAAgB;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,SAAS,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,YAAY,CAAC;IACjE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAC5B,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,KAAK,CAAC,EAClB,EAAE,EACF,GAAG,EACH,KAAK,EACL,MAAM,EACN,SAAc,EACd,IAAY,EACZ,KAAK,EACL,QAAgB,EAChB,SAAmB,EACnB,cAAyB,EACzB,KAAK,EACL,QAAe,EACf,GAAG,KAAK,EACX,EAAE,gBAAgB,2CAoSlB"}
|
package/dist/components/Image.js
CHANGED
|
@@ -5,6 +5,26 @@ import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
|
|
5
5
|
import { Edit2, Loader2 } from 'lucide-react';
|
|
6
6
|
import { getImageTransform, getImageFilter } from '../utils/transforms';
|
|
7
7
|
import { getFallbackImageUrl } from '../utils/fallback';
|
|
8
|
+
export let authCache = null;
|
|
9
|
+
let authFetching = null;
|
|
10
|
+
export async function checkAuthOnce() {
|
|
11
|
+
if (authCache !== null)
|
|
12
|
+
return authCache;
|
|
13
|
+
if (authFetching)
|
|
14
|
+
return authFetching;
|
|
15
|
+
authFetching = fetch('/api/me')
|
|
16
|
+
.then(res => res.json())
|
|
17
|
+
.then(data => {
|
|
18
|
+
const isAuth = data.loggedIn && ['admin', 'dev'].includes(data.user?.role);
|
|
19
|
+
authCache = isAuth;
|
|
20
|
+
return isAuth;
|
|
21
|
+
})
|
|
22
|
+
.catch(() => {
|
|
23
|
+
authCache = false;
|
|
24
|
+
return false;
|
|
25
|
+
});
|
|
26
|
+
return authFetching;
|
|
27
|
+
}
|
|
8
28
|
export function Image({ id, alt, width, height, className = '', fill = false, sizes, priority = false, objectFit = 'cover', objectPosition = 'center', style, editable = true, ...props // Using rest for override props
|
|
9
29
|
}) {
|
|
10
30
|
// 1. State management (Local only, used if props are undefined)
|
|
@@ -65,18 +85,11 @@ export function Image({ id, alt, width, height, className = '', fill = false, si
|
|
|
65
85
|
setIsResolving(false);
|
|
66
86
|
}
|
|
67
87
|
}, []);
|
|
68
|
-
// 4. Auth Check (
|
|
88
|
+
// 4. Auth Check (Cached - only fetches once)
|
|
69
89
|
useEffect(() => {
|
|
70
90
|
if (!editable)
|
|
71
91
|
return;
|
|
72
|
-
|
|
73
|
-
.then(res => res.json())
|
|
74
|
-
.then(data => {
|
|
75
|
-
if (data.loggedIn && ['admin', 'dev'].includes(data.user?.role)) {
|
|
76
|
-
setIsAuthenticated(true);
|
|
77
|
-
}
|
|
78
|
-
})
|
|
79
|
-
.catch(() => setIsAuthenticated(false));
|
|
92
|
+
checkAuthOnce().then(setIsAuthenticated);
|
|
80
93
|
}, [editable]);
|
|
81
94
|
// 5. Stable Event Listener (Prevents closure staleness)
|
|
82
95
|
// Only fetch from API if props are not provided (parent controls the values)
|
|
@@ -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;AAuDH,MAAM,WAAW,qBAAqB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAClB;AAqBD,wBAAgB,gBAAgB,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,qBAAqB,2CAssBzE"}
|