@jhits/plugin-images 0.0.1
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 +47 -0
- 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 +122 -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.tsx +111 -0
- package/src/components/GlobalImageEditor.tsx +778 -0
- package/src/components/Image.tsx +177 -0
- package/src/components/ImagePicker.tsx +541 -0
- package/src/components/ImagesPluginInit.tsx +31 -0
- package/src/components/index.ts +7 -0
- package/src/config.ts +179 -0
- package/src/index.server.ts +11 -0
- package/src/index.tsx +56 -0
- package/src/init.tsx +58 -0
- package/src/types/index.ts +60 -0
- package/src/utils/fallback.ts +73 -0
- package/src/views/ImageManager.tsx +30 -0
package/src/config.ts
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin Images Configuration
|
|
3
|
+
* Automatically creates required API routes in client apps
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Automatically creates plugin-images API catch-all route
|
|
11
|
+
* This route forwards requests to the plugin's API router
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* @deprecated Routes are now handled by the unified /api/[pluginId]/[...path]/route.ts
|
|
15
|
+
* This function is kept for backwards compatibility but does nothing
|
|
16
|
+
*/
|
|
17
|
+
export function ensureImagesRoutes() {
|
|
18
|
+
// Routes are now handled by the unified /api/[pluginId]/[...path]/route.ts
|
|
19
|
+
// No need to generate individual routes anymore
|
|
20
|
+
return;
|
|
21
|
+
try {
|
|
22
|
+
// Find the host app directory (where next.config.ts is)
|
|
23
|
+
let appDir = process.cwd();
|
|
24
|
+
const possiblePaths = [
|
|
25
|
+
appDir,
|
|
26
|
+
join(appDir, '..'),
|
|
27
|
+
join(appDir, '..', '..'),
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
for (const basePath of possiblePaths) {
|
|
31
|
+
const configPath = join(basePath, 'next.config.ts');
|
|
32
|
+
if (existsSync(configPath)) {
|
|
33
|
+
appDir = basePath;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const apiDir = join(appDir, 'src', 'app', 'api');
|
|
39
|
+
const pluginImagesApiDir = join(apiDir, 'plugin-images', '[...path]');
|
|
40
|
+
const pluginImagesApiPath = join(pluginImagesApiDir, 'route.ts');
|
|
41
|
+
|
|
42
|
+
// Check if route already exists
|
|
43
|
+
if (existsSync(pluginImagesApiPath)) {
|
|
44
|
+
const fs = require('fs');
|
|
45
|
+
const existingContent = fs.readFileSync(pluginImagesApiPath, 'utf8');
|
|
46
|
+
if (existingContent.includes('@jhits/plugin-images') || existingContent.includes('plugin-images')) {
|
|
47
|
+
// Already set up, skip
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Create plugin-images API catch-all route
|
|
53
|
+
mkdirSync(pluginImagesApiDir, { recursive: true });
|
|
54
|
+
writeFileSync(pluginImagesApiPath, `// Auto-generated by @jhits/plugin-images - Images Plugin API
|
|
55
|
+
// This route is automatically created for the images plugin
|
|
56
|
+
import { NextRequest } from 'next/server';
|
|
57
|
+
import { handleImagesApi } from '@jhits/plugin-images/api';
|
|
58
|
+
|
|
59
|
+
export async function GET(
|
|
60
|
+
req: NextRequest,
|
|
61
|
+
{ params }: { params: Promise<{ path: string[] }> }
|
|
62
|
+
) {
|
|
63
|
+
const { path } = await params;
|
|
64
|
+
return handleImagesApi(req, path);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function POST(
|
|
68
|
+
req: NextRequest,
|
|
69
|
+
{ params }: { params: Promise<{ path: string[] }> }
|
|
70
|
+
) {
|
|
71
|
+
const { path } = await params;
|
|
72
|
+
return handleImagesApi(req, path);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function PUT(
|
|
76
|
+
req: NextRequest,
|
|
77
|
+
{ params }: { params: Promise<{ path: string[] }> }
|
|
78
|
+
) {
|
|
79
|
+
const { path } = await params;
|
|
80
|
+
return handleImagesApi(req, path);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function DELETE(
|
|
84
|
+
req: NextRequest,
|
|
85
|
+
{ params }: { params: Promise<{ path: string[] }> }
|
|
86
|
+
) {
|
|
87
|
+
const { path } = await params;
|
|
88
|
+
return handleImagesApi(req, path);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function PATCH(
|
|
92
|
+
req: NextRequest,
|
|
93
|
+
{ params }: { params: Promise<{ path: string[] }> }
|
|
94
|
+
) {
|
|
95
|
+
const { path } = await params;
|
|
96
|
+
return handleImagesApi(req, path);
|
|
97
|
+
}
|
|
98
|
+
`);
|
|
99
|
+
|
|
100
|
+
// Also create uploads route for serving files
|
|
101
|
+
const uploadsApiDir = join(apiDir, 'uploads', '[filename]');
|
|
102
|
+
const uploadsApiPath = join(uploadsApiDir, 'route.ts');
|
|
103
|
+
|
|
104
|
+
if (!existsSync(uploadsApiPath)) {
|
|
105
|
+
mkdirSync(uploadsApiDir, { recursive: true });
|
|
106
|
+
writeFileSync(uploadsApiPath, `// Auto-generated by @jhits/plugin-images - Image Uploads API
|
|
107
|
+
// This route is automatically created for the images plugin
|
|
108
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
109
|
+
import { readFile } from 'fs/promises';
|
|
110
|
+
import path from 'path';
|
|
111
|
+
import { existsSync } from 'fs';
|
|
112
|
+
|
|
113
|
+
export async function GET(
|
|
114
|
+
request: NextRequest,
|
|
115
|
+
{ params }: { params: Promise<{ filename: string }> }
|
|
116
|
+
) {
|
|
117
|
+
const { filename } = await params;
|
|
118
|
+
|
|
119
|
+
// Security: Prevent directory traversal (only allow the filename)
|
|
120
|
+
const sanitizedFilename = path.basename(filename);
|
|
121
|
+
const filePath = path.join(process.cwd(), 'data', 'uploads', sanitizedFilename);
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const fileBuffer = await readFile(filePath);
|
|
125
|
+
|
|
126
|
+
// Determine content type based on extension
|
|
127
|
+
const ext = path.extname(sanitizedFilename).toLowerCase();
|
|
128
|
+
let contentType = 'application/octet-stream';
|
|
129
|
+
if (ext === '.png') contentType = 'image/png';
|
|
130
|
+
else if (ext === '.jpg' || ext === '.jpeg') contentType = 'image/jpeg';
|
|
131
|
+
else if (ext === '.gif') contentType = 'image/gif';
|
|
132
|
+
else if (ext === '.webp') contentType = 'image/webp';
|
|
133
|
+
else if (ext === '.svg') contentType = 'image/svg+xml';
|
|
134
|
+
|
|
135
|
+
return new NextResponse(fileBuffer, {
|
|
136
|
+
headers: {
|
|
137
|
+
'Content-Type': contentType,
|
|
138
|
+
'Cache-Control': 'public, max-age=31536000, immutable',
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
} catch (e) {
|
|
142
|
+
console.error('File serving error:', e);
|
|
143
|
+
return new NextResponse('File not found', { status: 404 });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function DELETE(
|
|
148
|
+
request: NextRequest,
|
|
149
|
+
{ params }: { params: Promise<{ filename: string }> }
|
|
150
|
+
) {
|
|
151
|
+
const { filename } = await params;
|
|
152
|
+
|
|
153
|
+
if (!filename) {
|
|
154
|
+
return new NextResponse('Missing filename', { status: 400 });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const sanitizedFilename = path.basename(filename);
|
|
158
|
+
const filePath = path.join(process.cwd(), 'data', 'uploads', sanitizedFilename);
|
|
159
|
+
|
|
160
|
+
if (!existsSync(filePath)) {
|
|
161
|
+
return new NextResponse('File not found', { status: 404 });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const { unlink } = require('fs/promises');
|
|
166
|
+
await unlink(filePath);
|
|
167
|
+
return NextResponse.json({ success: true, message: \`"\${sanitizedFilename}" deleted successfully.\` });
|
|
168
|
+
} catch (error) {
|
|
169
|
+
console.error('Failed to delete file:', error);
|
|
170
|
+
return new NextResponse('Failed to delete file', { status: 500 });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
`);
|
|
174
|
+
}
|
|
175
|
+
} catch (error) {
|
|
176
|
+
// Ignore errors - route might already exist or app structure is different
|
|
177
|
+
console.warn('[plugin-images] Could not ensure images routes:', error);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin Images - Server-Only Entry Point
|
|
3
|
+
* This file exports only server-side API handlers
|
|
4
|
+
* Used by the dynamic plugin router via @jhits/plugin-images/server
|
|
5
|
+
*
|
|
6
|
+
* Note: This file is server-only (no 'use server' needed - that's only for Server Actions)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export { handleImagesApi as handleApi } from './api/router';
|
|
10
|
+
export { handleImagesApi } from './api/router'; // Keep original export for backward compatibility
|
|
11
|
+
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin Images - Main Entry Point
|
|
3
|
+
* Image management plugin for uploading, searching, and managing images
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import { ImageManagerView } from './views/ImageManager';
|
|
10
|
+
|
|
11
|
+
export interface PluginProps {
|
|
12
|
+
subPath: string[];
|
|
13
|
+
siteId: string;
|
|
14
|
+
locale: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function ImagesPlugin({ subPath, siteId, locale }: PluginProps) {
|
|
18
|
+
const route = subPath[0] || 'manager';
|
|
19
|
+
|
|
20
|
+
switch (route) {
|
|
21
|
+
case 'manager':
|
|
22
|
+
return <ImageManagerView siteId={siteId} locale={locale} />;
|
|
23
|
+
|
|
24
|
+
default:
|
|
25
|
+
return <ImageManagerView siteId={siteId} locale={locale} />;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Export for use as default
|
|
30
|
+
export { ImagesPlugin as Index };
|
|
31
|
+
|
|
32
|
+
// Export components for use in other plugins
|
|
33
|
+
export { ImagePicker } from './components/ImagePicker';
|
|
34
|
+
export { GlobalImageEditor } from './components/GlobalImageEditor';
|
|
35
|
+
export { ImagesPluginInit } from './components/ImagesPluginInit';
|
|
36
|
+
export { Image } from './components/Image';
|
|
37
|
+
export { BackgroundImage } from './components/BackgroundImage';
|
|
38
|
+
export type { ImagePickerProps, ImageMetadata } from './types';
|
|
39
|
+
export type { PluginImageProps } from './components/Image';
|
|
40
|
+
export type { BackgroundImageProps } from './components/BackgroundImage';
|
|
41
|
+
|
|
42
|
+
// Export initialization utility
|
|
43
|
+
export { initImagesPlugin } from './init';
|
|
44
|
+
export type { ImagesPluginConfig } from './init';
|
|
45
|
+
|
|
46
|
+
// Export utility functions
|
|
47
|
+
export {
|
|
48
|
+
getFallbackImageUrl,
|
|
49
|
+
isValidImageUrl,
|
|
50
|
+
getSafeImageUrl,
|
|
51
|
+
constructImageUrl
|
|
52
|
+
} from './utils/fallback';
|
|
53
|
+
|
|
54
|
+
// Note: API handlers are server-only and exported from ./index.ts (server entry point)
|
|
55
|
+
// They are NOT exported here to prevent client/server context mixing
|
|
56
|
+
|
package/src/init.tsx
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Images Plugin Initialization Utility
|
|
3
|
+
*
|
|
4
|
+
* Simple function to initialize the images plugin with client configuration.
|
|
5
|
+
* Call this once in your app (e.g., in root layout) to enable global image editing.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { initImagesPlugin } from '@jhits/plugin-images/init';
|
|
10
|
+
* import { imagesConfig } from '@/plugins/images-config';
|
|
11
|
+
*
|
|
12
|
+
* // Call once when your app loads
|
|
13
|
+
* initImagesPlugin(imagesConfig);
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
'use client';
|
|
18
|
+
|
|
19
|
+
export interface ImagesPluginConfig {
|
|
20
|
+
/** Enable global image editor (default: true) */
|
|
21
|
+
enabled?: boolean;
|
|
22
|
+
/** Custom styling for the editor modal */
|
|
23
|
+
className?: string;
|
|
24
|
+
/** Custom styling for the modal overlay */
|
|
25
|
+
overlayClassName?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Initialize the images plugin with client configuration
|
|
30
|
+
*
|
|
31
|
+
* This function sets up the window global that the plugin reads from automatically.
|
|
32
|
+
* Call this once when your app loads, before the plugin component is rendered.
|
|
33
|
+
*
|
|
34
|
+
* @param config - Images plugin configuration (enabled, styling, etc.)
|
|
35
|
+
*/
|
|
36
|
+
export function initImagesPlugin(config?: ImagesPluginConfig): void {
|
|
37
|
+
if (typeof window === 'undefined') {
|
|
38
|
+
// Server-side: no-op
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Initialize the global plugin props object if it doesn't exist
|
|
43
|
+
if (!(window as any).__JHITS_PLUGIN_PROPS__) {
|
|
44
|
+
(window as any).__JHITS_PLUGIN_PROPS__ = {};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Set images plugin configuration
|
|
48
|
+
(window as any).__JHITS_PLUGIN_PROPS__['plugin-images'] = {
|
|
49
|
+
enabled: config?.enabled !== undefined ? config.enabled : true,
|
|
50
|
+
className: config?.className,
|
|
51
|
+
overlayClassName: config?.overlayClassName,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
console.log('[ImagesPlugin] Initialized with config:', {
|
|
55
|
+
enabled: config?.enabled !== undefined ? config.enabled : true,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Plugin Types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface ImageMetadata {
|
|
6
|
+
/** Unique image ID */
|
|
7
|
+
id: string;
|
|
8
|
+
/** Original filename */
|
|
9
|
+
filename: string;
|
|
10
|
+
/** File path/URL */
|
|
11
|
+
url: string;
|
|
12
|
+
/** File size in bytes */
|
|
13
|
+
size: number;
|
|
14
|
+
/** MIME type */
|
|
15
|
+
mimeType: string;
|
|
16
|
+
/** Image dimensions */
|
|
17
|
+
width?: number;
|
|
18
|
+
height?: number;
|
|
19
|
+
/** Alt text */
|
|
20
|
+
alt?: string;
|
|
21
|
+
/** Upload timestamp */
|
|
22
|
+
uploadedAt: string;
|
|
23
|
+
/** Uploaded by user ID */
|
|
24
|
+
uploadedBy?: string;
|
|
25
|
+
/** Tags for searching */
|
|
26
|
+
tags?: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ImageUploadResponse {
|
|
30
|
+
success: boolean;
|
|
31
|
+
image?: ImageMetadata;
|
|
32
|
+
error?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ImageListResponse {
|
|
36
|
+
images: ImageMetadata[];
|
|
37
|
+
total: number;
|
|
38
|
+
page: number;
|
|
39
|
+
limit: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ImagePickerProps {
|
|
43
|
+
/** Current selected image URL */
|
|
44
|
+
value?: string;
|
|
45
|
+
/** Callback when image is selected */
|
|
46
|
+
onChange: (image: ImageMetadata | null) => void;
|
|
47
|
+
/** Whether dark mode is enabled */
|
|
48
|
+
darkMode?: boolean;
|
|
49
|
+
/** Show brightness and blur controls */
|
|
50
|
+
showEffects?: boolean;
|
|
51
|
+
/** Current brightness value (0-200, 100 = normal) */
|
|
52
|
+
brightness?: number;
|
|
53
|
+
/** Current blur value (0-20) */
|
|
54
|
+
blur?: number;
|
|
55
|
+
/** Callback when brightness changes */
|
|
56
|
+
onBrightnessChange?: (brightness: number) => void;
|
|
57
|
+
/** Callback when blur changes */
|
|
58
|
+
onBlurChange?: (blur: number) => void;
|
|
59
|
+
}
|
|
60
|
+
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fallback image utility
|
|
3
|
+
* Provides a default fallback image URL when images fail to load or are invalid
|
|
4
|
+
* Also handles URL construction for image filenames
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Returns the URL for the fallback "image not found" image
|
|
9
|
+
* Served from the plugin's API route
|
|
10
|
+
*/
|
|
11
|
+
export function getFallbackImageUrl(): string {
|
|
12
|
+
return '/api/plugin-images/fallback';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Constructs the full image URL from a filename or URL
|
|
17
|
+
* - If it's already a full URL (http://, https://, or starts with /), returns as-is
|
|
18
|
+
* - If it's a filename, constructs `/api/uploads/${filename}`
|
|
19
|
+
*/
|
|
20
|
+
export function constructImageUrl(src: string | null | undefined): string | null {
|
|
21
|
+
if (!src || typeof src !== 'string') {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// If it's already a full URL (absolute or relative), return as-is
|
|
26
|
+
if (src.startsWith('http://') || src.startsWith('https://') || src.startsWith('/')) {
|
|
27
|
+
return src;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Otherwise, it's a filename - construct the API URL
|
|
31
|
+
return `/api/uploads/${src}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Validates if a URL is valid and can be used with Next.js Image component
|
|
36
|
+
*/
|
|
37
|
+
export function isValidImageUrl(url: string | null | undefined): boolean {
|
|
38
|
+
if (!url || typeof url !== 'string') {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Check if it's a valid URL format
|
|
43
|
+
try {
|
|
44
|
+
// For relative URLs (starting with /), they're valid
|
|
45
|
+
if (url.startsWith('/')) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// For absolute URLs, validate the URL format
|
|
50
|
+
new URL(url);
|
|
51
|
+
return true;
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Gets a safe image URL with automatic URL construction and fallback
|
|
59
|
+
* - Constructs the full URL if src is a filename
|
|
60
|
+
* - Falls back to the plugin's fallback image if invalid or missing
|
|
61
|
+
*/
|
|
62
|
+
export function getSafeImageUrl(src: string | null | undefined): string {
|
|
63
|
+
// First, construct the URL if it's a filename
|
|
64
|
+
const constructedUrl = constructImageUrl(src);
|
|
65
|
+
|
|
66
|
+
// Then validate and return, or fallback
|
|
67
|
+
if (isValidImageUrl(constructedUrl)) {
|
|
68
|
+
return constructedUrl!;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return getFallbackImageUrl();
|
|
72
|
+
}
|
|
73
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Manager View
|
|
3
|
+
* Main view for managing uploaded images
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import React from 'react';
|
|
9
|
+
|
|
10
|
+
export interface ImageManagerViewProps {
|
|
11
|
+
siteId: string;
|
|
12
|
+
locale: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function ImageManagerView({ siteId, locale }: ImageManagerViewProps) {
|
|
16
|
+
return (
|
|
17
|
+
<div className="min-h-screen bg-dashboard-bg p-8">
|
|
18
|
+
<div className="max-w-7xl mx-auto">
|
|
19
|
+
<h1 className="text-4xl font-black uppercase tracking-tighter text-dashboard-text mb-8">
|
|
20
|
+
Image Manager
|
|
21
|
+
</h1>
|
|
22
|
+
<p className="text-sm text-neutral-600 dark:text-neutral-400 mb-8">
|
|
23
|
+
This plugin provides image upload and management functionality.
|
|
24
|
+
Use the ImagePicker component in other plugins to select images.
|
|
25
|
+
</p>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|