@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/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
+