@jhits/plugin-images 0.0.11 → 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
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
|
4
|
+
import { createPortal } from 'react-dom';
|
|
5
|
+
import { Search, Image as ImageIcon, Settings } from 'lucide-react';
|
|
6
|
+
import type { ImagePickerProps } from '../types';
|
|
7
|
+
import type { ImageMetadata } from '../types';
|
|
8
|
+
import { ImageEditor, type ImageEditorHandle } from './ImageEditor';
|
|
9
|
+
import { ImageBrowserModal } from './ImageBrowserModal';
|
|
10
|
+
import { useImagePicker } from '../hooks/useImagePicker';
|
|
11
|
+
import { getImageTransform, getImageFilter } from '../utils/transforms';
|
|
12
|
+
import { getFallbackImageUrl } from '../utils/fallback';
|
|
13
|
+
|
|
14
|
+
export function ImagePicker({
|
|
15
|
+
value, onChange, brightness = 100, blur = 0, scale = 1.0, positionX = 0, positionY = 0,
|
|
16
|
+
aspectRatio = '16/9', borderRadius = 'rounded-xl', onBrightnessChange, onBlurChange,
|
|
17
|
+
onScaleChange, onPositionXChange, onPositionYChange, onEditorSave, autoOpenEditor = false,
|
|
18
|
+
}: ImagePickerProps) {
|
|
19
|
+
|
|
20
|
+
const [transforms, setTransforms] = useState({ scale: scale >= 0.1 ? scale : 1.0, positionX, positionY });
|
|
21
|
+
const [isEditorOpen, setIsEditorOpen] = useState(false);
|
|
22
|
+
const [isBrowserOpen, setIsBrowserOpen] = useState(false);
|
|
23
|
+
const [previewBaseScale, setPreviewBaseScale] = useState<number | null>(null);
|
|
24
|
+
const [previewImageError, setPreviewImageError] = useState(false);
|
|
25
|
+
const [mounted, setMounted] = useState(false);
|
|
26
|
+
const [portalTarget, setPortalTarget] = useState<HTMLElement | null>(null);
|
|
27
|
+
const previewImageRef = useRef<HTMLImageElement>(null);
|
|
28
|
+
const previewContainerRef = useRef<HTMLDivElement>(null);
|
|
29
|
+
const editorRef = useRef<ImageEditorHandle | null>(null);
|
|
30
|
+
|
|
31
|
+
const { selectedImage, setSelectedImage } = useImagePicker({ value, images: [] });
|
|
32
|
+
|
|
33
|
+
// Reset preview error when selected image changes
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
setPreviewImageError(false);
|
|
36
|
+
}, [selectedImage?.id, selectedImage?.url]);
|
|
37
|
+
|
|
38
|
+
// Handle SSR & portal target - ensure we only render portal on client
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
setMounted(true);
|
|
41
|
+
|
|
42
|
+
if (typeof document === 'undefined') return;
|
|
43
|
+
|
|
44
|
+
// If we're inside the GlobalImageEditor overlay, portal into that instead of document.body
|
|
45
|
+
// This ensures the browser/editor modals share the same stacking context
|
|
46
|
+
const editorContainer = previewContainerRef.current?.closest('[data-image-editor="true"]') as HTMLElement | null;
|
|
47
|
+
if (editorContainer) {
|
|
48
|
+
setPortalTarget(editorContainer);
|
|
49
|
+
} else {
|
|
50
|
+
setPortalTarget(document.body);
|
|
51
|
+
}
|
|
52
|
+
}, []);
|
|
53
|
+
|
|
54
|
+
// Auto-open editor if requested (e.g., when opening from edit button)
|
|
55
|
+
// Only auto-open once when the prop first becomes true, not on every render
|
|
56
|
+
const hasAutoOpenedRef = useRef(false);
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (autoOpenEditor && selectedImage && !isEditorOpen && !hasAutoOpenedRef.current) {
|
|
59
|
+
// Small delay to ensure ImagePicker is fully rendered
|
|
60
|
+
const timer = setTimeout(() => {
|
|
61
|
+
setIsEditorOpen(true);
|
|
62
|
+
hasAutoOpenedRef.current = true;
|
|
63
|
+
}, 100);
|
|
64
|
+
return () => clearTimeout(timer);
|
|
65
|
+
}
|
|
66
|
+
// Reset the flag when autoOpenEditor becomes false
|
|
67
|
+
if (!autoOpenEditor) {
|
|
68
|
+
hasAutoOpenedRef.current = false;
|
|
69
|
+
}
|
|
70
|
+
}, [autoOpenEditor, selectedImage, isEditorOpen]);
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (!isEditorOpen) setTransforms({ scale: scale >= 0.1 ? scale : 1.0, positionX, positionY });
|
|
74
|
+
}, [scale, positionX, positionY, isEditorOpen]);
|
|
75
|
+
|
|
76
|
+
const calculatePreviewBaseScale = useCallback(() => {
|
|
77
|
+
if (!previewImageRef.current || !previewContainerRef.current) return;
|
|
78
|
+
const fit = Math.max(previewContainerRef.current.offsetWidth / previewImageRef.current.naturalWidth,
|
|
79
|
+
previewContainerRef.current.offsetHeight / previewImageRef.current.naturalHeight);
|
|
80
|
+
setPreviewBaseScale(fit);
|
|
81
|
+
}, []);
|
|
82
|
+
|
|
83
|
+
// Handle editor save - delegate to parent callbacks instead of saving directly
|
|
84
|
+
// This ensures all saves go through a single location (GlobalImageEditor.saveImageTransform)
|
|
85
|
+
const handleEditorSave = async () => {
|
|
86
|
+
if (!editorRef.current || !selectedImage) return;
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
// 1. Get current values from Editor UI (this will also call onBrightnessChange and onBlurChange)
|
|
90
|
+
const final = await editorRef.current.flushSave();
|
|
91
|
+
|
|
92
|
+
// 2. Normalize position values - if they're -50% (centering value), treat as 0
|
|
93
|
+
const normalizedPositionX = final.positionX === -50 ? 0 : final.positionX;
|
|
94
|
+
const normalizedPositionY = final.positionY === -50 ? 0 : final.positionY;
|
|
95
|
+
|
|
96
|
+
// 3. If onEditorSave is provided, use it exclusively to prevent duplicate saves
|
|
97
|
+
// Otherwise, update parent state through individual callbacks
|
|
98
|
+
console.log('[ImagePicker] handleEditorSave - final values:', final, 'has onEditorSave:', !!onEditorSave);
|
|
99
|
+
if (onEditorSave) {
|
|
100
|
+
// onEditorSave handles everything - don't call individual handlers to avoid duplicates
|
|
101
|
+
console.log('[ImagePicker] Calling onEditorSave with:', { scale: final.scale, positionX: normalizedPositionX, positionY: normalizedPositionY, brightness: final.brightness, blur: final.blur });
|
|
102
|
+
onEditorSave(final.scale, normalizedPositionX, normalizedPositionY, final.brightness, final.blur);
|
|
103
|
+
} else {
|
|
104
|
+
// Fallback: update parent state through individual callbacks
|
|
105
|
+
// Since onEditorSave is not provided, call all callbacks and they should handle saving
|
|
106
|
+
// The last one (onPositionYChange) will trigger the final save
|
|
107
|
+
console.log('[ImagePicker] No onEditorSave, using individual callbacks');
|
|
108
|
+
// Update scale first
|
|
109
|
+
onScaleChange?.(final.scale);
|
|
110
|
+
// Update positions - these will trigger saves
|
|
111
|
+
onPositionXChange?.(normalizedPositionX);
|
|
112
|
+
// Last callback - this should trigger the final save with all values
|
|
113
|
+
// We need to ensure this saves immediately with all final values including brightness/blur
|
|
114
|
+
onPositionYChange?.(normalizedPositionY);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 5. Close the editor
|
|
118
|
+
setIsEditorOpen(false);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.error('[ImagePicker] Failed to get editor values:', error);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
const aspectValue = useMemo(() => {
|
|
126
|
+
if (aspectRatio === 'auto') return undefined;
|
|
127
|
+
const [w, h] = aspectRatio.split('/').map(Number);
|
|
128
|
+
return w / h;
|
|
129
|
+
}, [aspectRatio]);
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<div className="space-y-6">
|
|
133
|
+
{selectedImage ? (
|
|
134
|
+
<div className="relative group max-w-md mx-auto">
|
|
135
|
+
<div className={`relative ${borderRadius} overflow-hidden border border-neutral-200 dark:border-neutral-800 bg-neutral-50 dark:bg-neutral-900/50 shadow-lg transition-all duration-300 hover:shadow-xl`} style={{ aspectRatio: aspectValue, width: '100%' }}>
|
|
136
|
+
<div ref={previewContainerRef} className="relative w-full h-full overflow-hidden">
|
|
137
|
+
{previewImageError ? (
|
|
138
|
+
<img
|
|
139
|
+
src={getFallbackImageUrl()}
|
|
140
|
+
alt={selectedImage.filename || 'Image not found'}
|
|
141
|
+
className="absolute max-w-none"
|
|
142
|
+
style={{
|
|
143
|
+
top: '50%', left: '50%', width: 'auto', height: 'auto',
|
|
144
|
+
minWidth: '100%', minHeight: '100%',
|
|
145
|
+
transform: 'translate(-50%, -50%)',
|
|
146
|
+
transformOrigin: 'center center',
|
|
147
|
+
objectFit: 'cover',
|
|
148
|
+
}}
|
|
149
|
+
/>
|
|
150
|
+
) : (
|
|
151
|
+
<img
|
|
152
|
+
ref={previewImageRef}
|
|
153
|
+
src={selectedImage.url}
|
|
154
|
+
alt={selectedImage.filename}
|
|
155
|
+
className="absolute max-w-none"
|
|
156
|
+
onLoad={calculatePreviewBaseScale}
|
|
157
|
+
onError={() => setPreviewImageError(true)}
|
|
158
|
+
style={{
|
|
159
|
+
top: '50%', left: '50%', width: 'auto', height: 'auto',
|
|
160
|
+
minWidth: '100%', minHeight: '100%',
|
|
161
|
+
filter: getImageFilter(brightness, blur),
|
|
162
|
+
transform: previewBaseScale ? getImageTransform({ scale: transforms.scale, positionX: transforms.positionX, positionY: transforms.positionY, baseScale: previewBaseScale }, true) : 'translate(-50%, -50%)',
|
|
163
|
+
transformOrigin: 'center center',
|
|
164
|
+
}}
|
|
165
|
+
/>
|
|
166
|
+
)}
|
|
167
|
+
</div>
|
|
168
|
+
{/* Overlay on hover */}
|
|
169
|
+
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/5 transition-colors duration-300 pointer-events-none" />
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
) : (
|
|
173
|
+
<div
|
|
174
|
+
onClick={() => setIsBrowserOpen(true)}
|
|
175
|
+
className="aspect-video bg-gradient-to-br from-neutral-50 to-neutral-100 dark:from-neutral-900 dark:to-neutral-950 rounded-2xl border-2 border-dashed border-neutral-300 dark:border-neutral-800 flex flex-col items-center justify-center text-neutral-400 hover:border-primary hover:text-primary cursor-pointer transition-all duration-300 hover:shadow-lg group"
|
|
176
|
+
>
|
|
177
|
+
<div className="p-4 bg-white/50 dark:bg-neutral-800/50 rounded-full mb-3 group-hover:scale-110 transition-transform duration-300">
|
|
178
|
+
<ImageIcon size={28} className="group-hover:scale-110 transition-transform duration-300" />
|
|
179
|
+
</div>
|
|
180
|
+
<span className="text-xs font-bold uppercase tracking-[0.15em] group-hover:tracking-[0.2em] transition-all duration-300">Select Image</span>
|
|
181
|
+
</div>
|
|
182
|
+
)}
|
|
183
|
+
|
|
184
|
+
<div className="flex flex-wrap items-center justify-center gap-3">
|
|
185
|
+
<button
|
|
186
|
+
onClick={() => setIsBrowserOpen(true)}
|
|
187
|
+
className="flex items-center justify-center gap-2 px-5 py-2.5 bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-xl text-sm font-semibold text-neutral-700 dark:text-neutral-300 hover:bg-neutral-50 dark:hover:bg-neutral-750 hover:border-neutral-300 dark:hover:border-neutral-600 transition-all duration-200 shadow-sm hover:shadow-md active:scale-[0.98]"
|
|
188
|
+
>
|
|
189
|
+
<Search size={16} />
|
|
190
|
+
Browse
|
|
191
|
+
</button>
|
|
192
|
+
<button
|
|
193
|
+
onClick={() => setIsEditorOpen(true)}
|
|
194
|
+
disabled={!selectedImage}
|
|
195
|
+
className="flex items-center justify-center gap-2 px-5 py-2.5 bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-xl text-sm font-semibold text-neutral-700 dark:text-neutral-300 hover:bg-neutral-50 dark:hover:bg-neutral-750 hover:border-neutral-300 dark:hover:border-neutral-600 transition-all duration-200 shadow-sm hover:shadow-md active:scale-[0.98] disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:shadow-sm disabled:hover:scale-100"
|
|
196
|
+
>
|
|
197
|
+
<Settings size={16} />
|
|
198
|
+
Edit
|
|
199
|
+
</button>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
{isEditorOpen && selectedImage && mounted && portalTarget && createPortal(
|
|
203
|
+
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-neutral-950/80 dark:bg-neutral-950/90 backdrop-blur-md p-4 animate-in fade-in duration-200">
|
|
204
|
+
<div className="w-full max-w-4xl bg-white dark:bg-neutral-900 rounded-3xl overflow-hidden shadow-2xl dark:shadow-neutral-950/50 flex flex-col max-h-[85vh] border border-neutral-200 dark:border-neutral-800 animate-in zoom-in-95 duration-300">
|
|
205
|
+
<div className="flex-1 overflow-hidden p-4 lg:p-6">
|
|
206
|
+
<ImageEditor
|
|
207
|
+
ref={editorRef}
|
|
208
|
+
imageUrl={selectedImage.url}
|
|
209
|
+
scale={transforms.scale}
|
|
210
|
+
positionX={transforms.positionX}
|
|
211
|
+
positionY={transforms.positionY}
|
|
212
|
+
brightness={brightness}
|
|
213
|
+
blur={blur}
|
|
214
|
+
onScaleChange={(s) => setTransforms(t => ({ ...t, scale: s }))}
|
|
215
|
+
onPositionChange={(x, y) => {
|
|
216
|
+
// Only update local state during drag - don't trigger saves
|
|
217
|
+
// Saves will happen when editor closes via onEditorSave
|
|
218
|
+
setTransforms(t => ({ ...t, positionX: x, positionY: y }));
|
|
219
|
+
}}
|
|
220
|
+
onBrightnessChange={onBrightnessChange}
|
|
221
|
+
onBlurChange={onBlurChange}
|
|
222
|
+
aspectRatio={aspectRatio}
|
|
223
|
+
borderRadius={borderRadius}
|
|
224
|
+
/>
|
|
225
|
+
</div>
|
|
226
|
+
<div className="px-6 py-5 border-t border-neutral-200 dark:border-neutral-800 bg-neutral-50/80 dark:bg-neutral-900/80 backdrop-blur-sm flex justify-end gap-3">
|
|
227
|
+
<button
|
|
228
|
+
onClick={() => setIsEditorOpen(false)}
|
|
229
|
+
className="px-6 py-2.5 bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-xl font-semibold text-neutral-700 dark:text-neutral-200 hover:bg-neutral-50 dark:hover:bg-neutral-700 transition-all duration-200 shadow-sm hover:shadow-md active:scale-[0.98]"
|
|
230
|
+
>
|
|
231
|
+
Cancel
|
|
232
|
+
</button>
|
|
233
|
+
<button
|
|
234
|
+
onClick={handleEditorSave}
|
|
235
|
+
className="px-8 py-2.5 bg-primary text-white rounded-xl font-semibold shadow-md hover:shadow-lg hover:bg-primary/90 transition-all duration-200 active:scale-[0.98]"
|
|
236
|
+
>
|
|
237
|
+
Done
|
|
238
|
+
</button>
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
</div>,
|
|
242
|
+
portalTarget
|
|
243
|
+
)}
|
|
244
|
+
|
|
245
|
+
<ImageBrowserModal
|
|
246
|
+
isOpen={isBrowserOpen}
|
|
247
|
+
onClose={() => setIsBrowserOpen(false)}
|
|
248
|
+
onSelectImage={(image) => {
|
|
249
|
+
setSelectedImage(image);
|
|
250
|
+
onChange?.(image);
|
|
251
|
+
setIsBrowserOpen(false);
|
|
252
|
+
}}
|
|
253
|
+
selectedImageId={(() => {
|
|
254
|
+
// Use resolved image's filename/URL if available, otherwise fall back to value
|
|
255
|
+
// This ensures semantic IDs are resolved to actual filenames for matching
|
|
256
|
+
if (selectedImage) {
|
|
257
|
+
return selectedImage.filename || selectedImage.url || selectedImage.id || value;
|
|
258
|
+
}
|
|
259
|
+
return value;
|
|
260
|
+
})()}
|
|
261
|
+
darkMode={false}
|
|
262
|
+
/>
|
|
263
|
+
</div>
|
|
264
|
+
);
|
|
265
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Images Plugin Initialization Component
|
|
3
|
+
*
|
|
4
|
+
* This component reads from window.__JHITS_PLUGIN_PROPS__['plugin-images']
|
|
5
|
+
* and renders the GlobalImageEditor if enabled.
|
|
6
|
+
*
|
|
7
|
+
* Render this once in your app layout after calling initImagesPlugin().
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Images Plugin Initialization Component
|
|
11
|
+
*
|
|
12
|
+
* Renders the global image editor if enabled in the plugin configuration.
|
|
13
|
+
* This component should be rendered in your app layout.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```tsx
|
|
17
|
+
* import { ImagesPluginInit } from '@jhits/plugin-images';
|
|
18
|
+
*
|
|
19
|
+
* // After calling initImagesPlugin() in a useEffect or script
|
|
20
|
+
* <ImagesPluginInit />
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare function ImagesPluginInit(): import("react/jsx-runtime").JSX.Element;
|
|
24
|
+
//# sourceMappingURL=ImagesPluginInit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ImagesPluginInit.d.ts","sourceRoot":"","sources":["ImagesPluginInit.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,4CAE/B"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Images Plugin Initialization Component
|
|
3
|
+
*
|
|
4
|
+
* This component reads from window.__JHITS_PLUGIN_PROPS__['plugin-images']
|
|
5
|
+
* and renders the GlobalImageEditor if enabled.
|
|
6
|
+
*
|
|
7
|
+
* Render this once in your app layout after calling initImagesPlugin().
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use client';
|
|
11
|
+
|
|
12
|
+
import { GlobalImageEditor } from './GlobalImageEditor';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Images Plugin Initialization Component
|
|
16
|
+
*
|
|
17
|
+
* Renders the global image editor if enabled in the plugin configuration.
|
|
18
|
+
* This component should be rendered in your app layout.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```tsx
|
|
22
|
+
* import { ImagesPluginInit } from '@jhits/plugin-images';
|
|
23
|
+
*
|
|
24
|
+
* // After calling initImagesPlugin() in a useEffect or script
|
|
25
|
+
* <ImagesPluginInit />
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export function ImagesPluginInit() {
|
|
29
|
+
return <GlobalImageEditor />;
|
|
30
|
+
}
|
|
31
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Picker Component Exports
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { ImagePicker } from './ImagePicker';
|
|
6
|
+
export { ImageEditor } from './ImageEditor';
|
|
7
|
+
export { ImageEffectsPanel } from './ImageEffectsPanel';
|
|
8
|
+
export { ImageBrowserModal } from './ImageBrowserModal';
|
|
9
|
+
export type { ImagePickerProps } from '../types';
|
|
10
|
+
|
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,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook for Image Picker Logic
|
|
3
|
+
*/
|
|
4
|
+
import type { ImageMetadata } from '../types';
|
|
5
|
+
export interface UseImagePickerOptions {
|
|
6
|
+
value?: string;
|
|
7
|
+
images: ImageMetadata[];
|
|
8
|
+
}
|
|
9
|
+
export declare function useImagePicker({ value, images }: UseImagePickerOptions): {
|
|
10
|
+
selectedImage: ImageMetadata | null;
|
|
11
|
+
setSelectedImage: import("react").Dispatch<import("react").SetStateAction<ImageMetadata | null>>;
|
|
12
|
+
uploading: boolean;
|
|
13
|
+
fileInputRef: import("react").RefObject<HTMLInputElement | null>;
|
|
14
|
+
handleFileSelect: (e?: React.ChangeEvent<HTMLInputElement> | {
|
|
15
|
+
target: {
|
|
16
|
+
files: File[] | null;
|
|
17
|
+
};
|
|
18
|
+
}) => Promise<any>;
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=useImagePicker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useImagePicker.d.ts","sourceRoot":"","sources":["useImagePicker.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAE9C,MAAM,WAAW,qBAAqB;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,aAAa,EAAE,CAAC;CAC3B;AAED,wBAAgB,cAAc,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,qBAAqB;;;;;2BA6R/B,KAAK,CAAC,WAAW,CAAC,gBAAgB,CAAC,GAAG;QAAE,MAAM,EAAE;YAAE,KAAK,EAAE,IAAI,EAAE,GAAG,IAAI,CAAA;SAAE,CAAA;KAAE;EA8CjH"}
|