@jhits/plugin-images 0.0.10 → 0.0.12
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/package.json +3 -2
- package/src/api/fallback/route.ts +69 -0
- package/src/api/index.ts +10 -0
- package/src/api/list/index.ts +96 -0
- package/src/api/resolve/route.ts +241 -0
- package/src/api/router.ts +85 -0
- package/src/api/upload/index.ts +88 -0
- package/src/api/uploads/[filename]/route.ts +93 -0
- package/src/api-server.ts +11 -0
- package/src/assets/noimagefound.jpg +0 -0
- package/src/components/BackgroundImage.d.ts +11 -0
- package/src/components/BackgroundImage.d.ts.map +1 -0
- package/src/components/BackgroundImage.tsx +92 -0
- package/src/components/GlobalImageEditor/config.d.ts +9 -0
- package/src/components/GlobalImageEditor/config.d.ts.map +1 -0
- package/src/components/GlobalImageEditor/config.ts +21 -0
- package/src/components/GlobalImageEditor/eventHandlers.d.ts +20 -0
- package/src/components/GlobalImageEditor/eventHandlers.d.ts.map +1 -0
- package/src/components/GlobalImageEditor/eventHandlers.ts +267 -0
- package/src/components/GlobalImageEditor/imageDetection.d.ts +16 -0
- package/src/components/GlobalImageEditor/imageDetection.d.ts.map +1 -0
- package/src/components/GlobalImageEditor/imageDetection.ts +160 -0
- package/src/components/GlobalImageEditor/imageSetup.d.ts +9 -0
- package/src/components/GlobalImageEditor/imageSetup.d.ts.map +1 -0
- package/src/components/GlobalImageEditor/imageSetup.ts +306 -0
- package/src/components/GlobalImageEditor/saveLogic.d.ts +26 -0
- package/src/components/GlobalImageEditor/saveLogic.d.ts.map +1 -0
- package/src/components/GlobalImageEditor/saveLogic.ts +133 -0
- package/src/components/GlobalImageEditor/stylingDetection.d.ts +9 -0
- package/src/components/GlobalImageEditor/stylingDetection.d.ts.map +1 -0
- package/src/components/GlobalImageEditor/stylingDetection.ts +122 -0
- package/src/components/GlobalImageEditor/transformParsing.d.ts +16 -0
- package/src/components/GlobalImageEditor/transformParsing.d.ts.map +1 -0
- package/src/components/GlobalImageEditor/transformParsing.ts +83 -0
- package/src/components/GlobalImageEditor/types.d.ts +36 -0
- package/src/components/GlobalImageEditor/types.d.ts.map +1 -0
- package/src/components/GlobalImageEditor/types.ts +39 -0
- package/src/components/GlobalImageEditor.d.ts +8 -0
- package/src/components/GlobalImageEditor.d.ts.map +1 -0
- package/src/components/GlobalImageEditor.tsx +327 -0
- package/src/components/Image.d.ts +22 -0
- package/src/components/Image.d.ts.map +1 -0
- package/src/components/Image.tsx +343 -0
- package/src/components/ImageBrowserModal.d.ts +13 -0
- package/src/components/ImageBrowserModal.d.ts.map +1 -0
- package/src/components/ImageBrowserModal.tsx +837 -0
- package/src/components/ImageEditor.d.ts +27 -0
- package/src/components/ImageEditor.d.ts.map +1 -0
- package/src/components/ImageEditor.tsx +323 -0
- package/src/components/ImageEffectsPanel.tsx +116 -0
- package/src/components/ImagePicker.d.ts +3 -0
- package/src/components/ImagePicker.d.ts.map +1 -0
- package/src/components/ImagePicker.tsx +265 -0
- package/src/components/ImagesPluginInit.d.ts +24 -0
- package/src/components/ImagesPluginInit.d.ts.map +1 -0
- package/src/components/ImagesPluginInit.tsx +31 -0
- package/src/components/index.ts +10 -0
- package/src/config.ts +179 -0
- package/src/hooks/useImagePicker.d.ts +20 -0
- package/src/hooks/useImagePicker.d.ts.map +1 -0
- package/src/hooks/useImagePicker.ts +344 -0
- package/src/index.server.ts +12 -0
- package/src/index.tsx +56 -0
- package/src/init.tsx +58 -0
- package/src/types/index.d.ts +80 -0
- package/src/types/index.d.ts.map +1 -0
- package/src/types/index.ts +84 -0
- package/src/utils/fallback.d.ts +27 -0
- package/src/utils/fallback.d.ts.map +1 -0
- package/src/utils/fallback.ts +73 -0
- package/src/utils/transforms.d.ts +26 -0
- package/src/utils/transforms.d.ts.map +1 -0
- package/src/utils/transforms.ts +54 -0
- package/src/views/ImageManager.d.ts +10 -0
- package/src/views/ImageManager.d.ts.map +1 -0
- package/src/views/ImageManager.tsx +30 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jhits/plugin-images",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
4
4
|
"description": "Image management and storage plugin for the JHITS ecosystem",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"lucide-react": "^0.564.0",
|
|
29
29
|
"mongodb": "^7.1.0",
|
|
30
|
-
"@jhits/plugin-core": "0.0.
|
|
30
|
+
"@jhits/plugin-core": "0.0.9"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"next": ">=15.0.0",
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"typescript": "^5.9.3"
|
|
49
49
|
},
|
|
50
50
|
"files": [
|
|
51
|
+
"src",
|
|
51
52
|
"dist",
|
|
52
53
|
"package.json"
|
|
53
54
|
],
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fallback Image API Route
|
|
3
|
+
* Serves the default "image not found" fallback image
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
7
|
+
import { readFile } from 'fs/promises';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
|
|
10
|
+
export async function GET(request: NextRequest) {
|
|
11
|
+
try {
|
|
12
|
+
// Try multiple possible paths for the fallback image
|
|
13
|
+
const possiblePaths = [
|
|
14
|
+
// Path in workspace root (monorepo development)
|
|
15
|
+
path.join(process.cwd(), 'packages', 'plugin-images', 'src', 'assets', 'noimagefound.jpg'),
|
|
16
|
+
// Path in node_modules (production)
|
|
17
|
+
path.join(process.cwd(), 'node_modules', '@jhits', 'plugin-images', 'src', 'assets', 'noimagefound.jpg'),
|
|
18
|
+
// Fallback to app's public folder if plugin image not found
|
|
19
|
+
path.join(process.cwd(), 'public', 'noimagefound.jpg'),
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
let filePath: string | null = null;
|
|
23
|
+
for (const testPath of possiblePaths) {
|
|
24
|
+
try {
|
|
25
|
+
await readFile(testPath);
|
|
26
|
+
filePath = testPath;
|
|
27
|
+
break;
|
|
28
|
+
} catch {
|
|
29
|
+
// Continue to next path
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!filePath) {
|
|
34
|
+
// If no image found, return a simple placeholder SVG
|
|
35
|
+
const svgPlaceholder = `<svg width="400" height="300" xmlns="http://www.w3.org/2000/svg">
|
|
36
|
+
<rect width="400" height="300" fill="#f3f4f6"/>
|
|
37
|
+
<text x="50%" y="50%" text-anchor="middle" dominant-baseline="middle" font-family="Arial" font-size="16" fill="#9ca3af">Image not found</text>
|
|
38
|
+
</svg>`;
|
|
39
|
+
return new NextResponse(svgPlaceholder, {
|
|
40
|
+
headers: {
|
|
41
|
+
'Content-Type': 'image/svg+xml',
|
|
42
|
+
'Cache-Control': 'public, max-age=3600',
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const fileBuffer = await readFile(filePath);
|
|
48
|
+
|
|
49
|
+
return new NextResponse(fileBuffer, {
|
|
50
|
+
headers: {
|
|
51
|
+
'Content-Type': 'image/jpeg',
|
|
52
|
+
'Cache-Control': 'public, max-age=31536000, immutable',
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
} catch (error: any) {
|
|
56
|
+
console.error('[FallbackImage] Error serving fallback image:', error);
|
|
57
|
+
// Return SVG placeholder on error
|
|
58
|
+
const svgPlaceholder = `<svg width="400" height="300" xmlns="http://www.w3.org/2000/svg">
|
|
59
|
+
<rect width="400" height="300" fill="#f3f4f6"/>
|
|
60
|
+
<text x="50%" y="50%" text-anchor="middle" dominant-baseline="middle" font-family="Arial" font-size="16" fill="#9ca3af">Image not found</text>
|
|
61
|
+
</svg>`;
|
|
62
|
+
return new NextResponse(svgPlaceholder, {
|
|
63
|
+
headers: {
|
|
64
|
+
'Content-Type': 'image/svg+xml',
|
|
65
|
+
'Cache-Control': 'public, max-age=3600',
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
package/src/api/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin Images API Exports
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { handleImagesApi } from './router';
|
|
6
|
+
export { GET as GET_LIST } from './list';
|
|
7
|
+
export { POST as POST_UPLOAD } from './upload';
|
|
8
|
+
export { GET as GET_RESOLVE, POST as POST_RESOLVE } from './resolve/route';
|
|
9
|
+
export { GET as GET_UPLOADS, DELETE as DELETE_UPLOADS } from './uploads/[filename]/route';
|
|
10
|
+
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image List API Handler
|
|
3
|
+
* GET /api/plugin-images/list - Returns paginated list of uploaded images
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
7
|
+
import { readdir, stat } from 'fs/promises';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
|
|
10
|
+
const uploadsDir = path.join(process.cwd(), 'data', 'uploads');
|
|
11
|
+
|
|
12
|
+
export async function GET(request: NextRequest) {
|
|
13
|
+
try {
|
|
14
|
+
const { searchParams } = new URL(request.url);
|
|
15
|
+
const page = parseInt(searchParams.get('page') || '1');
|
|
16
|
+
const limit = parseInt(searchParams.get('limit') || '20');
|
|
17
|
+
const search = searchParams.get('search') || '';
|
|
18
|
+
|
|
19
|
+
// Read uploads directory
|
|
20
|
+
let files: string[] = [];
|
|
21
|
+
try {
|
|
22
|
+
files = await readdir(uploadsDir);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
// Directory doesn't exist yet
|
|
25
|
+
return NextResponse.json({
|
|
26
|
+
images: [],
|
|
27
|
+
total: 0,
|
|
28
|
+
page: 1,
|
|
29
|
+
limit,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Filter image files and get metadata
|
|
34
|
+
const imageExtensions = ['.jpg', '.jpeg', '.png', '.webp', '.gif'];
|
|
35
|
+
const imageFiles = files.filter(file => {
|
|
36
|
+
const ext = path.extname(file).toLowerCase();
|
|
37
|
+
return imageExtensions.includes(ext);
|
|
38
|
+
});
|
|
39
|
+
|
|
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
|
+
const filesWithStats = await Promise.all(
|
|
47
|
+
filteredFiles.map(async (filename) => {
|
|
48
|
+
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
|
+
};
|
|
55
|
+
})
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
filesWithStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
59
|
+
|
|
60
|
+
// Paginate
|
|
61
|
+
const start = (page - 1) * limit;
|
|
62
|
+
const end = start + limit;
|
|
63
|
+
const paginatedFiles = filesWithStats.slice(start, end);
|
|
64
|
+
|
|
65
|
+
// Build image metadata
|
|
66
|
+
const images = paginatedFiles.map(({ filename, mtime, size }) => {
|
|
67
|
+
const ext = path.extname(filename).toLowerCase();
|
|
68
|
+
const mimeType = ext === '.png' ? 'image/png' :
|
|
69
|
+
ext === '.webp' ? 'image/webp' :
|
|
70
|
+
ext === '.gif' ? 'image/gif' : 'image/jpeg';
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
id: filename,
|
|
74
|
+
filename,
|
|
75
|
+
url: `/api/uploads/${filename}`,
|
|
76
|
+
size,
|
|
77
|
+
mimeType,
|
|
78
|
+
uploadedAt: mtime.toISOString(),
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return NextResponse.json({
|
|
83
|
+
images,
|
|
84
|
+
total: filteredFiles.length,
|
|
85
|
+
page,
|
|
86
|
+
limit,
|
|
87
|
+
});
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error('List images error:', error);
|
|
90
|
+
return NextResponse.json(
|
|
91
|
+
{ error: 'Failed to list images' },
|
|
92
|
+
{ status: 500 }
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image ID Resolution API Route
|
|
3
|
+
* Resolves semantic image IDs to actual filenames
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
7
|
+
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
|
|
10
|
+
const mappingsPath = path.join(process.cwd(), 'data', 'image-mappings.json');
|
|
11
|
+
|
|
12
|
+
// Simple mutex to prevent concurrent writes
|
|
13
|
+
let writeLock = false;
|
|
14
|
+
const writeQueue: Array<() => void> = [];
|
|
15
|
+
|
|
16
|
+
async function acquireLock(): Promise<() => void> {
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
if (!writeLock) {
|
|
19
|
+
writeLock = true;
|
|
20
|
+
resolve(() => {
|
|
21
|
+
writeLock = false;
|
|
22
|
+
const next = writeQueue.shift();
|
|
23
|
+
if (next) next();
|
|
24
|
+
});
|
|
25
|
+
} else {
|
|
26
|
+
writeQueue.push(() => {
|
|
27
|
+
writeLock = true;
|
|
28
|
+
resolve(() => {
|
|
29
|
+
writeLock = false;
|
|
30
|
+
const next = writeQueue.shift();
|
|
31
|
+
if (next) next();
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function readMappingsSafely(): Promise<Record<string, any>> {
|
|
39
|
+
try {
|
|
40
|
+
const content = await readFile(mappingsPath, 'utf-8');
|
|
41
|
+
// Try to parse, if it fails, try to recover
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(content);
|
|
44
|
+
} catch (parseError) {
|
|
45
|
+
console.error('JSON parse error, attempting recovery:', parseError);
|
|
46
|
+
// Try to extract valid JSON by finding the last complete object
|
|
47
|
+
const lines = content.split('\n');
|
|
48
|
+
let validContent = '';
|
|
49
|
+
let braceCount = 0;
|
|
50
|
+
let inString = false;
|
|
51
|
+
let escapeNext = false;
|
|
52
|
+
|
|
53
|
+
for (const line of lines) {
|
|
54
|
+
for (let i = 0; i < line.length; i++) {
|
|
55
|
+
const char = line[i];
|
|
56
|
+
if (escapeNext) {
|
|
57
|
+
escapeNext = false;
|
|
58
|
+
validContent += char;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (char === '\\') {
|
|
62
|
+
escapeNext = true;
|
|
63
|
+
validContent += char;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (char === '"') {
|
|
67
|
+
inString = !inString;
|
|
68
|
+
validContent += char;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (!inString) {
|
|
72
|
+
if (char === '{') braceCount++;
|
|
73
|
+
if (char === '}') braceCount--;
|
|
74
|
+
}
|
|
75
|
+
validContent += char;
|
|
76
|
+
}
|
|
77
|
+
validContent += '\n';
|
|
78
|
+
// If we've closed all braces, we might have valid JSON
|
|
79
|
+
if (braceCount === 0 && validContent.trim().endsWith('}')) {
|
|
80
|
+
try {
|
|
81
|
+
return JSON.parse(validContent.trim());
|
|
82
|
+
} catch {
|
|
83
|
+
// Continue trying
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// If recovery failed, return empty object
|
|
88
|
+
console.error('Failed to recover JSON, using empty mappings');
|
|
89
|
+
return {};
|
|
90
|
+
}
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.error('Failed to read mappings file:', error);
|
|
93
|
+
return {};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function ensureMappingsFile() {
|
|
98
|
+
try {
|
|
99
|
+
const dir = path.dirname(mappingsPath);
|
|
100
|
+
await mkdir(dir, { recursive: true });
|
|
101
|
+
|
|
102
|
+
// Check if file exists, if not create empty mappings
|
|
103
|
+
try {
|
|
104
|
+
await readFile(mappingsPath);
|
|
105
|
+
} catch {
|
|
106
|
+
await writeFile(mappingsPath, JSON.stringify({}, null, 2));
|
|
107
|
+
}
|
|
108
|
+
} catch (error) {
|
|
109
|
+
// Ignore errors
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* GET /api/plugin-images/resolve?id=home-about-card
|
|
115
|
+
* Resolves a semantic image ID to the actual filename
|
|
116
|
+
*/
|
|
117
|
+
export async function GET(request: NextRequest) {
|
|
118
|
+
try {
|
|
119
|
+
await ensureMappingsFile();
|
|
120
|
+
|
|
121
|
+
const { searchParams } = new URL(request.url);
|
|
122
|
+
const id = searchParams.get('id');
|
|
123
|
+
|
|
124
|
+
if (!id) {
|
|
125
|
+
return NextResponse.json(
|
|
126
|
+
{ error: 'Missing id parameter' },
|
|
127
|
+
{ status: 400 }
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const mappings = await readMappingsSafely();
|
|
132
|
+
const mapping = mappings[id];
|
|
133
|
+
|
|
134
|
+
if (!mapping) {
|
|
135
|
+
return NextResponse.json(
|
|
136
|
+
{ error: 'Image ID not found', id },
|
|
137
|
+
{ status: 404 }
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Support both old format (just filename string) and new format (object)
|
|
142
|
+
const filename = typeof mapping === 'string' ? mapping : mapping.filename;
|
|
143
|
+
const brightness = typeof mapping === 'object' ? (mapping.brightness ?? 100) : 100;
|
|
144
|
+
const blur = typeof mapping === 'object' ? (mapping.blur ?? 0) : 0;
|
|
145
|
+
// Ensure scale is within valid range (0.1 to 5.0), default to 1.0 if invalid
|
|
146
|
+
const rawScale = typeof mapping === 'object' ? (mapping.scale ?? 1.0) : 1.0;
|
|
147
|
+
const scale = rawScale > 0 && rawScale <= 5.0 ? Math.max(0.1, rawScale) : 1.0;
|
|
148
|
+
const positionX = typeof mapping === 'object' ? (mapping.positionX ?? 0) : 0;
|
|
149
|
+
const positionY = typeof mapping === 'object' ? (mapping.positionY ?? 0) : 0;
|
|
150
|
+
|
|
151
|
+
return NextResponse.json({
|
|
152
|
+
id,
|
|
153
|
+
filename,
|
|
154
|
+
brightness,
|
|
155
|
+
blur,
|
|
156
|
+
scale,
|
|
157
|
+
positionX,
|
|
158
|
+
positionY,
|
|
159
|
+
url: `/api/uploads/${encodeURIComponent(filename)}`,
|
|
160
|
+
});
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.error('Resolve error:', error);
|
|
163
|
+
return NextResponse.json(
|
|
164
|
+
{ error: 'Failed to resolve image ID' },
|
|
165
|
+
{ status: 500 }
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* POST /api/plugin-images/resolve
|
|
172
|
+
* Sets or updates the mapping between an ID and filename
|
|
173
|
+
* Body: { id: string, filename: string }
|
|
174
|
+
*/
|
|
175
|
+
export async function POST(request: NextRequest) {
|
|
176
|
+
try {
|
|
177
|
+
await ensureMappingsFile();
|
|
178
|
+
|
|
179
|
+
const body = await request.json();
|
|
180
|
+
const { id, filename, brightness, blur, scale, positionX, positionY } = body;
|
|
181
|
+
|
|
182
|
+
if (!id || !filename) {
|
|
183
|
+
return NextResponse.json(
|
|
184
|
+
{ error: 'Missing id or filename' },
|
|
185
|
+
{ status: 400 }
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Acquire write lock to prevent concurrent writes
|
|
190
|
+
const releaseLock = await acquireLock();
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const mappings = await readMappingsSafely();
|
|
194
|
+
|
|
195
|
+
// Store as object with optional brightness, blur, scale, and position
|
|
196
|
+
// Merge with existing values to avoid overwriting fields that aren't provided
|
|
197
|
+
// Ensure scale is within valid range (0.1 to 5.0)
|
|
198
|
+
const safeScale = scale !== undefined ? Math.max(0.1, Math.min(5.0, scale)) : undefined;
|
|
199
|
+
const existing = mappings[id] || {};
|
|
200
|
+
mappings[id] = {
|
|
201
|
+
...existing, // Preserve existing values
|
|
202
|
+
filename, // Always update filename
|
|
203
|
+
...(brightness !== undefined && { brightness }),
|
|
204
|
+
...(blur !== undefined && { blur }),
|
|
205
|
+
...(safeScale !== undefined && { scale: safeScale }),
|
|
206
|
+
...(positionX !== undefined && { positionX }),
|
|
207
|
+
...(positionY !== undefined && { positionY }),
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// Write atomically using a temporary file
|
|
211
|
+
const tempPath = mappingsPath + '.tmp';
|
|
212
|
+
await writeFile(tempPath, JSON.stringify(mappings, null, 2), 'utf-8');
|
|
213
|
+
|
|
214
|
+
// Atomic rename (this is atomic on most filesystems)
|
|
215
|
+
const { rename } = await import('fs/promises');
|
|
216
|
+
await rename(tempPath, mappingsPath);
|
|
217
|
+
} finally {
|
|
218
|
+
releaseLock();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
console.log('SAVING TO DB:', { id, scale, brightness, blur, positionX, positionY });
|
|
222
|
+
|
|
223
|
+
return NextResponse.json({
|
|
224
|
+
success: true,
|
|
225
|
+
id,
|
|
226
|
+
filename,
|
|
227
|
+
brightness: brightness ?? 100,
|
|
228
|
+
blur: blur ?? 0,
|
|
229
|
+
scale: scale ?? 1.0,
|
|
230
|
+
positionX: positionX ?? 0,
|
|
231
|
+
positionY: positionY ?? 0,
|
|
232
|
+
});
|
|
233
|
+
} catch (error) {
|
|
234
|
+
console.error('Set mapping error:', error);
|
|
235
|
+
return NextResponse.json(
|
|
236
|
+
{ error: 'Failed to set image mapping' },
|
|
237
|
+
{ status: 500 }
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use server';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Plugin Images API Router
|
|
5
|
+
* Centralized API handler for all images plugin routes
|
|
6
|
+
*
|
|
7
|
+
* This router handles requests to /api/plugin-images/*
|
|
8
|
+
* and routes them to the appropriate handler
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Handle images API requests
|
|
15
|
+
* Routes requests to appropriate handlers based on path
|
|
16
|
+
*/
|
|
17
|
+
export async function handleImagesApi(
|
|
18
|
+
req: NextRequest,
|
|
19
|
+
path: string[]
|
|
20
|
+
): Promise<NextResponse> {
|
|
21
|
+
const method = req.method;
|
|
22
|
+
const route = path[0] || '';
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
// Route: /api/plugin-images/list (list images)
|
|
26
|
+
if (!route || route === 'list') {
|
|
27
|
+
if (method === 'GET') {
|
|
28
|
+
const listModule = await import('./list');
|
|
29
|
+
return await listModule.GET(req);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Route: /api/plugin-images/upload (upload image)
|
|
34
|
+
else if (route === 'upload') {
|
|
35
|
+
if (method === 'POST') {
|
|
36
|
+
const uploadModule = await import('./upload');
|
|
37
|
+
return await uploadModule.POST(req);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Route: /api/plugin-images/resolve (resolve image ID)
|
|
42
|
+
else if (route === 'resolve') {
|
|
43
|
+
if (method === 'GET' || method === 'POST') {
|
|
44
|
+
const resolveModule = await import('./resolve/route');
|
|
45
|
+
if (method === 'GET') {
|
|
46
|
+
return await resolveModule.GET(req);
|
|
47
|
+
}
|
|
48
|
+
return await resolveModule.POST(req);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Route: /api/plugin-images/uploads/[filename] (serve/delete image)
|
|
53
|
+
else if (route === 'uploads' && path[1]) {
|
|
54
|
+
const filename = path[1];
|
|
55
|
+
if (method === 'GET' || method === 'DELETE') {
|
|
56
|
+
const uploadsModule = await import('./uploads/[filename]/route');
|
|
57
|
+
if (method === 'GET') {
|
|
58
|
+
return await uploadsModule.GET(req, { params: Promise.resolve({ filename }) });
|
|
59
|
+
}
|
|
60
|
+
return await uploadsModule.DELETE(req, { params: Promise.resolve({ filename }) });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Route: /api/plugin-images/fallback (serve fallback image)
|
|
65
|
+
else if (route === 'fallback') {
|
|
66
|
+
if (method === 'GET') {
|
|
67
|
+
const fallbackModule = await import('./fallback/route');
|
|
68
|
+
return await fallbackModule.GET(req);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Method not allowed
|
|
73
|
+
return NextResponse.json(
|
|
74
|
+
{ error: `Method ${method} not allowed for route: ${route || '/'}` },
|
|
75
|
+
{ status: 405 }
|
|
76
|
+
);
|
|
77
|
+
} catch (error: any) {
|
|
78
|
+
console.error('[ImagesApiRouter] Error:', error);
|
|
79
|
+
return NextResponse.json(
|
|
80
|
+
{ error: error.message || 'Internal server error' },
|
|
81
|
+
{ status: 500 }
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Upload API Handler
|
|
3
|
+
* POST /api/plugin-images/upload - Handles image file uploads
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
7
|
+
import { writeFile, mkdir } from 'fs/promises';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { randomBytes } from 'crypto';
|
|
10
|
+
|
|
11
|
+
// Ensure uploads directory exists
|
|
12
|
+
const uploadsDir = path.join(process.cwd(), 'data', 'uploads');
|
|
13
|
+
|
|
14
|
+
async function ensureUploadsDir() {
|
|
15
|
+
try {
|
|
16
|
+
await mkdir(uploadsDir, { recursive: true });
|
|
17
|
+
} catch (error) {
|
|
18
|
+
// Directory might already exist
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function POST(request: NextRequest) {
|
|
23
|
+
try {
|
|
24
|
+
await ensureUploadsDir();
|
|
25
|
+
|
|
26
|
+
const formData = await request.formData();
|
|
27
|
+
const file = formData.get('file') as File;
|
|
28
|
+
|
|
29
|
+
if (!file) {
|
|
30
|
+
return NextResponse.json(
|
|
31
|
+
{ success: false, error: 'No file provided' },
|
|
32
|
+
{ status: 400 }
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Validate file type
|
|
37
|
+
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif'];
|
|
38
|
+
if (!allowedTypes.includes(file.type)) {
|
|
39
|
+
return NextResponse.json(
|
|
40
|
+
{ success: false, error: 'Invalid file type. Only images are allowed.' },
|
|
41
|
+
{ status: 400 }
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Validate file size (max 10MB)
|
|
46
|
+
const maxSize = 10 * 1024 * 1024; // 10MB
|
|
47
|
+
if (file.size > maxSize) {
|
|
48
|
+
return NextResponse.json(
|
|
49
|
+
{ success: false, error: 'File size exceeds 10MB limit' },
|
|
50
|
+
{ status: 400 }
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Generate unique filename
|
|
55
|
+
const ext = path.extname(file.name);
|
|
56
|
+
const uniqueId = randomBytes(16).toString('hex');
|
|
57
|
+
const timestamp = Date.now();
|
|
58
|
+
const uniqueFilename = `${timestamp}-${uniqueId}${ext}`;
|
|
59
|
+
const filePath = path.join(uploadsDir, uniqueFilename);
|
|
60
|
+
|
|
61
|
+
// Save file
|
|
62
|
+
const bytes = await file.arrayBuffer();
|
|
63
|
+
const buffer = Buffer.from(bytes);
|
|
64
|
+
await writeFile(filePath, buffer);
|
|
65
|
+
|
|
66
|
+
// Get image dimensions (basic - could use sharp for better handling)
|
|
67
|
+
const imageMetadata = {
|
|
68
|
+
id: uniqueFilename,
|
|
69
|
+
filename: file.name,
|
|
70
|
+
url: `/api/uploads/${uniqueFilename}`,
|
|
71
|
+
size: file.size,
|
|
72
|
+
mimeType: file.type,
|
|
73
|
+
uploadedAt: new Date().toISOString(),
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
return NextResponse.json({
|
|
77
|
+
success: true,
|
|
78
|
+
image: imageMetadata,
|
|
79
|
+
});
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error('Upload error:', error);
|
|
82
|
+
return NextResponse.json(
|
|
83
|
+
{ success: false, error: 'Failed to upload image' },
|
|
84
|
+
{ status: 500 }
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|