@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.
@@ -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;AAMxD,wBAAsB,GAAG,CAAC,OAAO,EAAE,WAAW;;;;;;;;;;;;;;IAmF7C"}
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"}
@@ -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') || '20');
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
- // Apply search filter if provided
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
- const stats = await stat(filePath);
43
- return {
44
- filename,
45
- mtime: stats.mtime,
46
- size: stats.size,
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
- filesWithStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
50
- // Paginate
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 = filesWithStats.slice(start, end);
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) {
@@ -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,CAgEvB"}
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"}
@@ -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,2CAqEtB"}
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
- const checkUser = async () => {
11
- try {
12
- const res = await fetch('/api/me');
13
- const data = await res.json();
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;AAeH,wBAAgB,iBAAiB,mDAkThC"}
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 res = await fetch('/api/me');
33
- const data = await res.json();
34
- if (data.loggedIn && (data.user?.role === 'admin' || data.user?.role === 'dev')) {
35
- setUserRole(data.user.role);
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,4 +1,6 @@
1
1
  import React from 'react';
2
+ export declare let authCache: boolean | null;
3
+ export declare function checkAuthOnce(): Promise<boolean>;
2
4
  export interface PluginImageProps {
3
5
  id: string;
4
6
  alt: string;
@@ -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,2CA2SlB"}
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"}
@@ -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 (Single mount only)
88
+ // 4. Auth Check (Cached - only fetches once)
69
89
  useEffect(() => {
70
90
  if (!editable)
71
91
  return;
72
- fetch('/api/me')
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,6 +1,6 @@
1
1
  /**
2
2
  * Image Manager View
3
- * Main view for managing uploaded images
3
+ * Full-featured image management with CRUD operations and usage tracking
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;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"}