@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
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import NextImage from 'next/image';
|
|
4
|
+
import React, { useState, useEffect, useCallback } from 'react';
|
|
5
|
+
import { Edit2, Loader2 } from 'lucide-react';
|
|
6
|
+
|
|
7
|
+
export interface PluginImageProps {
|
|
8
|
+
id: string;
|
|
9
|
+
alt: string;
|
|
10
|
+
width?: number;
|
|
11
|
+
height?: number;
|
|
12
|
+
className?: string;
|
|
13
|
+
fill?: boolean;
|
|
14
|
+
sizes?: string;
|
|
15
|
+
priority?: boolean;
|
|
16
|
+
objectFit?: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
|
|
17
|
+
objectPosition?: string;
|
|
18
|
+
style?: React.CSSProperties;
|
|
19
|
+
editable?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function Image({
|
|
23
|
+
id,
|
|
24
|
+
alt,
|
|
25
|
+
width,
|
|
26
|
+
height,
|
|
27
|
+
className = '',
|
|
28
|
+
fill = false,
|
|
29
|
+
sizes,
|
|
30
|
+
priority = false,
|
|
31
|
+
objectFit = 'cover',
|
|
32
|
+
objectPosition = 'center',
|
|
33
|
+
style,
|
|
34
|
+
editable = true,
|
|
35
|
+
}: PluginImageProps) {
|
|
36
|
+
const [resolvedFilename, setResolvedFilename] = useState<string | null>(null);
|
|
37
|
+
const [isResolving, setIsResolving] = useState(true);
|
|
38
|
+
const [brightness, setBrightness] = useState<number>(100);
|
|
39
|
+
const [blur, setBlur] = useState<number>(0);
|
|
40
|
+
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
|
|
41
|
+
const [authChecked, setAuthChecked] = useState<boolean>(false);
|
|
42
|
+
|
|
43
|
+
// Check authentication status if editable is true (default)
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (editable && !authChecked) {
|
|
46
|
+
const checkAuth = async () => {
|
|
47
|
+
try {
|
|
48
|
+
const res = await fetch('/api/me');
|
|
49
|
+
const data = await res.json();
|
|
50
|
+
if (data.loggedIn && (data.user?.role === 'admin' || data.user?.role === 'dev')) {
|
|
51
|
+
setIsAuthenticated(true);
|
|
52
|
+
}
|
|
53
|
+
} catch (error) {
|
|
54
|
+
// User is not authenticated or API call failed
|
|
55
|
+
setIsAuthenticated(false);
|
|
56
|
+
} finally {
|
|
57
|
+
setAuthChecked(true);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
checkAuth();
|
|
61
|
+
} else if (!editable) {
|
|
62
|
+
// If explicitly set to false, skip auth check
|
|
63
|
+
setAuthChecked(true);
|
|
64
|
+
}
|
|
65
|
+
}, [editable, authChecked]);
|
|
66
|
+
|
|
67
|
+
// Only allow editing if editable is true AND user is authenticated
|
|
68
|
+
const canEdit = editable && isAuthenticated;
|
|
69
|
+
|
|
70
|
+
const resolveImage = useCallback(() => {
|
|
71
|
+
const looksLikeFilename = /\.(jpg|jpeg|png|webp|gif|svg)$/i.test(id) || /^\d+-/.test(id);
|
|
72
|
+
if (looksLikeFilename) {
|
|
73
|
+
setResolvedFilename(id);
|
|
74
|
+
setIsResolving(false);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
fetch(`/api/plugin-images/resolve?id=${encodeURIComponent(id)}`)
|
|
79
|
+
.then(res => res.ok ? res.json() : { filename: id, brightness: 100, blur: 0 })
|
|
80
|
+
.then(data => {
|
|
81
|
+
setResolvedFilename(data.filename || id);
|
|
82
|
+
setBrightness(data.brightness ?? 100);
|
|
83
|
+
setBlur(data.blur ?? 0);
|
|
84
|
+
setIsResolving(false);
|
|
85
|
+
})
|
|
86
|
+
.catch(() => {
|
|
87
|
+
setResolvedFilename(id);
|
|
88
|
+
setBrightness(100);
|
|
89
|
+
setBlur(0);
|
|
90
|
+
setIsResolving(false);
|
|
91
|
+
});
|
|
92
|
+
}, [id]);
|
|
93
|
+
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
resolveImage();
|
|
96
|
+
const handleMappingUpdate = (e: any) => {
|
|
97
|
+
if (e.detail?.id === id) {
|
|
98
|
+
setIsResolving(true);
|
|
99
|
+
if (e.detail.brightness !== undefined) setBrightness(e.detail.brightness);
|
|
100
|
+
if (e.detail.blur !== undefined) setBlur(e.detail.blur);
|
|
101
|
+
setTimeout(() => resolveImage(), 100);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
window.addEventListener('image-mapping-updated', handleMappingUpdate as EventListener);
|
|
105
|
+
return () => window.removeEventListener('image-mapping-updated', handleMappingUpdate as EventListener);
|
|
106
|
+
}, [id, resolveImage]);
|
|
107
|
+
|
|
108
|
+
const src = resolvedFilename
|
|
109
|
+
? `/api/uploads/${encodeURIComponent(resolvedFilename)}`
|
|
110
|
+
: `/api/uploads/${encodeURIComponent(id)}`;
|
|
111
|
+
|
|
112
|
+
// Logic: If the user passes 'w-full' or 'h-full', or explicit 'fill',
|
|
113
|
+
// we must use Next.js fill mode to make object-fit work.
|
|
114
|
+
const shouldFill = fill || className.includes('w-full') || className.includes('h-full');
|
|
115
|
+
|
|
116
|
+
const filterStyle = brightness !== 100 || blur !== 0
|
|
117
|
+
? `brightness(${brightness}%) blur(${blur}px)`
|
|
118
|
+
: undefined;
|
|
119
|
+
|
|
120
|
+
const handleEditClick = (e: React.MouseEvent) => {
|
|
121
|
+
e.preventDefault();
|
|
122
|
+
e.stopPropagation();
|
|
123
|
+
window.dispatchEvent(new CustomEvent('open-image-editor', {
|
|
124
|
+
detail: { id, currentBrightness: brightness, currentBlur: blur }
|
|
125
|
+
}));
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<div
|
|
130
|
+
className={`group/image relative overflow-hidden transition-all duration-300 ${className}`}
|
|
131
|
+
style={{
|
|
132
|
+
...style,
|
|
133
|
+
position: 'relative',
|
|
134
|
+
filter: filterStyle || style?.filter,
|
|
135
|
+
}}
|
|
136
|
+
data-image-id={id}
|
|
137
|
+
>
|
|
138
|
+
{/* RESOLVING STATE */}
|
|
139
|
+
{isResolving && (
|
|
140
|
+
<div className="absolute inset-0 z-20 flex items-center justify-center bg-neutral-100 dark:bg-neutral-800 animate-pulse">
|
|
141
|
+
<Loader2 className="w-5 h-5 animate-spin text-neutral-400" />
|
|
142
|
+
</div>
|
|
143
|
+
)}
|
|
144
|
+
|
|
145
|
+
{/* THE IMAGE */}
|
|
146
|
+
<NextImage
|
|
147
|
+
src={src}
|
|
148
|
+
alt={alt}
|
|
149
|
+
width={!shouldFill ? width : undefined}
|
|
150
|
+
height={!shouldFill ? height : undefined}
|
|
151
|
+
fill={shouldFill}
|
|
152
|
+
sizes={sizes || (shouldFill ? "100vw" : undefined)}
|
|
153
|
+
priority={priority}
|
|
154
|
+
className={`transition-all duration-500 ease-in-out ${editable ? 'group-hover/image:scale-105' : ''}`}
|
|
155
|
+
style={{
|
|
156
|
+
objectFit: objectFit,
|
|
157
|
+
objectPosition: objectPosition,
|
|
158
|
+
}}
|
|
159
|
+
/>
|
|
160
|
+
|
|
161
|
+
{/* EDIT OVERLAY - Dashboard Theme */}
|
|
162
|
+
{canEdit && (
|
|
163
|
+
<div
|
|
164
|
+
onClick={handleEditClick}
|
|
165
|
+
className="absolute inset-0 z-30 flex items-center justify-center opacity-0 group-hover/image:opacity-100 transition-all duration-300 cursor-pointer"
|
|
166
|
+
>
|
|
167
|
+
<div className="absolute inset-0 bg-neutral-900/40 dark:bg-neutral-900/60 backdrop-blur-[2px]" />
|
|
168
|
+
|
|
169
|
+
<div className="relative flex items-center gap-2 px-4 py-2.5 bg-neutral-100 dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 rounded-full shadow-2xl scale-90 group-hover/image:scale-100 transition-all duration-300 border border-neutral-300 dark:border-neutral-700">
|
|
170
|
+
<Edit2 size={14} strokeWidth={2.5} className="text-primary" />
|
|
171
|
+
<span className="text-[10px] font-black uppercase tracking-widest">Edit</span>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
)}
|
|
175
|
+
</div>
|
|
176
|
+
);
|
|
177
|
+
}
|