@jhits/plugin-images 0.0.14 → 0.0.15
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/dist/components/Image.d.ts +1 -6
- package/dist/components/Image.d.ts.map +1 -1
- package/dist/components/Image.js +86 -202
- package/dist/components/ImageEditor.d.ts.map +1 -1
- package/dist/components/ImageEditor.js +21 -125
- package/dist/components/ImagePicker.d.ts.map +1 -1
- package/dist/components/ImagePicker.js +6 -59
- package/dist/utils/fallback.d.ts +9 -4
- package/dist/utils/fallback.d.ts.map +1 -1
- package/dist/utils/fallback.js +40 -12
- package/dist/utils/transforms.d.ts.map +1 -1
- package/dist/utils/transforms.js +7 -10
- package/dist/views/ImageManager/components/CleanupLibraryModal.d.ts +12 -0
- package/dist/views/ImageManager/components/CleanupLibraryModal.d.ts.map +1 -0
- package/dist/views/ImageManager/components/CleanupLibraryModal.js +7 -0
- package/dist/views/ImageManager/components/DeleteImageModal.d.ts +15 -0
- package/dist/views/ImageManager/components/DeleteImageModal.d.ts.map +1 -0
- package/dist/views/ImageManager/components/DeleteImageModal.js +8 -0
- package/dist/views/ImageManager/components/ImageGrid.d.ts +12 -0
- package/dist/views/ImageManager/components/ImageGrid.d.ts.map +1 -0
- package/dist/views/ImageManager/components/ImageGrid.js +15 -0
- package/dist/views/ImageManager/components/ImageManagerHeader.d.ts +11 -0
- package/dist/views/ImageManager/components/ImageManagerHeader.d.ts.map +1 -0
- package/dist/views/ImageManager/components/ImageManagerHeader.js +6 -0
- package/dist/views/ImageManager/components/ImageManagerStats.d.ts +8 -0
- package/dist/views/ImageManager/components/ImageManagerStats.d.ts.map +1 -0
- package/dist/views/ImageManager/components/ImageManagerStats.js +6 -0
- package/dist/views/ImageManager/components/ImageManagerToolbar.d.ts +9 -0
- package/dist/views/ImageManager/components/ImageManagerToolbar.d.ts.map +1 -0
- package/dist/views/ImageManager/components/ImageManagerToolbar.js +10 -0
- package/dist/views/ImageManager/components/ImageTable.d.ts +13 -0
- package/dist/views/ImageManager/components/ImageTable.d.ts.map +1 -0
- package/dist/views/ImageManager/components/ImageTable.js +13 -0
- package/dist/views/ImageManager/types.d.ts +26 -0
- package/dist/views/ImageManager/types.d.ts.map +1 -0
- package/dist/views/ImageManager/types.js +1 -0
- package/dist/views/ImageManager.d.ts +1 -1
- package/dist/views/ImageManager.d.ts.map +1 -1
- package/dist/views/ImageManager.js +28 -52
- package/package.json +10 -9
- package/src/components/Image.tsx +107 -262
- package/src/components/ImageEditor.tsx +31 -193
- package/src/components/ImagePicker.tsx +22 -107
- package/src/utils/fallback.ts +46 -13
- package/src/utils/transforms.ts +9 -12
- package/src/views/ImageManager/components/CleanupLibraryModal.tsx +96 -0
- package/src/views/ImageManager/components/DeleteImageModal.tsx +144 -0
- package/src/views/ImageManager/components/ImageGrid.tsx +119 -0
- package/src/views/ImageManager/components/ImageManagerHeader.tsx +72 -0
- package/src/views/ImageManager/components/ImageManagerStats.tsx +60 -0
- package/src/views/ImageManager/components/ImageManagerToolbar.tsx +60 -0
- package/src/views/ImageManager/components/ImageTable.tsx +120 -0
- package/src/views/ImageManager/types.ts +27 -0
- package/src/views/ImageManager.tsx +103 -571
- package/src/components/BackgroundImage.d.ts +0 -11
- package/src/components/BackgroundImage.d.ts.map +0 -1
- package/src/components/GlobalImageEditor/config.d.ts +0 -9
- package/src/components/GlobalImageEditor/config.d.ts.map +0 -1
- package/src/components/GlobalImageEditor/eventHandlers.d.ts +0 -20
- package/src/components/GlobalImageEditor/eventHandlers.d.ts.map +0 -1
- package/src/components/GlobalImageEditor/imageDetection.d.ts +0 -16
- package/src/components/GlobalImageEditor/imageDetection.d.ts.map +0 -1
- package/src/components/GlobalImageEditor/imageSetup.d.ts +0 -9
- package/src/components/GlobalImageEditor/imageSetup.d.ts.map +0 -1
- package/src/components/GlobalImageEditor/saveLogic.d.ts +0 -26
- package/src/components/GlobalImageEditor/saveLogic.d.ts.map +0 -1
- package/src/components/GlobalImageEditor/stylingDetection.d.ts +0 -9
- package/src/components/GlobalImageEditor/stylingDetection.d.ts.map +0 -1
- package/src/components/GlobalImageEditor/transformParsing.d.ts +0 -16
- package/src/components/GlobalImageEditor/transformParsing.d.ts.map +0 -1
- package/src/components/GlobalImageEditor/types.d.ts +0 -36
- package/src/components/GlobalImageEditor/types.d.ts.map +0 -1
- package/src/components/GlobalImageEditor.d.ts +0 -8
- package/src/components/GlobalImageEditor.d.ts.map +0 -1
- package/src/components/Image.d.ts +0 -22
- package/src/components/Image.d.ts.map +0 -1
- package/src/components/ImageBrowserModal.d.ts +0 -13
- package/src/components/ImageBrowserModal.d.ts.map +0 -1
- package/src/components/ImageEditor.d.ts +0 -27
- package/src/components/ImageEditor.d.ts.map +0 -1
- package/src/components/ImagePicker.d.ts +0 -3
- package/src/components/ImagePicker.d.ts.map +0 -1
- package/src/components/ImagesPluginInit.d.ts +0 -24
- package/src/components/ImagesPluginInit.d.ts.map +0 -1
- package/src/hooks/useImagePicker.d.ts +0 -20
- package/src/hooks/useImagePicker.d.ts.map +0 -1
- package/src/types/index.d.ts +0 -80
- package/src/types/index.d.ts.map +0 -1
- package/src/utils/fallback.d.ts +0 -27
- package/src/utils/fallback.d.ts.map +0 -1
- package/src/utils/transforms.d.ts +0 -26
- package/src/utils/transforms.d.ts.map +0 -1
- package/src/views/ImageManager.d.ts +0 -10
- package/src/views/ImageManager.d.ts.map +0 -1
package/src/utils/fallback.ts
CHANGED
|
@@ -4,35 +4,65 @@
|
|
|
4
4
|
* Also handles URL construction for image filenames
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
// If set, this URL will be used as a fallback for static images, served directly by Nginx/CDN.
|
|
8
|
+
// Example: NEXT_PUBLIC_STATIC_IMAGE_BASE_URL=https://cdn.yourdomain.com/static-uploads
|
|
9
|
+
const STATIC_IMAGE_BASE_URL = process.env.NEXT_PUBLIC_STATIC_IMAGE_BASE_URL || '';
|
|
10
|
+
|
|
7
11
|
/**
|
|
8
|
-
* Returns the URL for the fallback "image not found" image
|
|
9
|
-
*
|
|
12
|
+
* Returns the URL for the fallback "image not found" image.
|
|
13
|
+
* Uses a static file from public/ so it works without any API/dashboard dependency.
|
|
10
14
|
*/
|
|
11
15
|
export function getFallbackImageUrl(): string {
|
|
12
|
-
return
|
|
16
|
+
return `/noimagefound.jpg`;
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
/**
|
|
16
20
|
* Constructs the full image URL from a filename or URL
|
|
17
21
|
* - If it's already a full URL (http://, https://, or starts with /), returns as-is
|
|
18
|
-
* - If it's a filename, constructs
|
|
22
|
+
* - If it's a filename, constructs relative URL to /api/uploads/
|
|
19
23
|
*/
|
|
20
24
|
export function constructImageUrl(src: string | null | undefined): string | null {
|
|
21
25
|
if (!src || typeof src !== 'string') {
|
|
22
26
|
return null;
|
|
23
27
|
}
|
|
24
28
|
|
|
25
|
-
// If it's already
|
|
26
|
-
if (src.startsWith('http://') || src.startsWith('https://')
|
|
29
|
+
// If it's already an absolute URL (http:// or https://), return as-is
|
|
30
|
+
if (src.startsWith('http://') || src.startsWith('https://')) {
|
|
31
|
+
return src;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// If it starts with /api/uploads, return as-is (already relative API path)
|
|
35
|
+
if (src.startsWith('/api/uploads/')) {
|
|
27
36
|
return src;
|
|
28
37
|
}
|
|
29
38
|
|
|
30
|
-
//
|
|
39
|
+
// If it starts with / but is not an API path, it might be a local asset, keep as-is
|
|
40
|
+
if (src.startsWith('/')) {
|
|
41
|
+
return src;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Otherwise, it's a raw filename - construct the relative API URL
|
|
31
45
|
return `/api/uploads/${src}`;
|
|
32
46
|
}
|
|
33
47
|
|
|
34
48
|
/**
|
|
35
|
-
*
|
|
49
|
+
* Constructs a direct static URL for the image, using STATIC_IMAGE_BASE_URL if available.
|
|
50
|
+
* This is intended for use as a fallback when API-driven serving fails.
|
|
51
|
+
*/
|
|
52
|
+
export function constructStaticImageUrl(filename: string | null | undefined): string | null {
|
|
53
|
+
if (!filename || !STATIC_IMAGE_BASE_URL) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
// Ensure filename doesn't start with /api/uploads/ if it was passed through from constructImageUrl
|
|
57
|
+
const cleanFilename = filename.startsWith('/api/uploads/') ? filename.replace('/api/uploads/', '') : filename;
|
|
58
|
+
|
|
59
|
+
// Ensure base URL ends with a slash
|
|
60
|
+
const baseUrl = STATIC_IMAGE_BASE_URL.endsWith('/') ? STATIC_IMAGE_BASE_URL : `${STATIC_IMAGE_BASE_URL}/`;
|
|
61
|
+
return `${baseUrl}${cleanFilename}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Validates if a URL is valid
|
|
36
66
|
*/
|
|
37
67
|
export function isValidImageUrl(url: string | null | undefined): boolean {
|
|
38
68
|
if (!url || typeof url !== 'string') {
|
|
@@ -41,14 +71,18 @@ export function isValidImageUrl(url: string | null | undefined): boolean {
|
|
|
41
71
|
|
|
42
72
|
// Check if it's a valid URL format
|
|
43
73
|
try {
|
|
44
|
-
//
|
|
74
|
+
// Absolute URLs (including http/https)
|
|
75
|
+
if (url.startsWith('http://') || url.startsWith('https://')) {
|
|
76
|
+
new URL(url);
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Relative URLs starting with /
|
|
45
81
|
if (url.startsWith('/')) {
|
|
46
82
|
return true;
|
|
47
83
|
}
|
|
48
84
|
|
|
49
|
-
|
|
50
|
-
new URL(url);
|
|
51
|
-
return true;
|
|
85
|
+
return false;
|
|
52
86
|
} catch {
|
|
53
87
|
return false;
|
|
54
88
|
}
|
|
@@ -70,4 +104,3 @@ export function getSafeImageUrl(src: string | null | undefined): string {
|
|
|
70
104
|
|
|
71
105
|
return getFallbackImageUrl();
|
|
72
106
|
}
|
|
73
|
-
|
package/src/utils/transforms.ts
CHANGED
|
@@ -29,23 +29,20 @@ export function getImageTransform(
|
|
|
29
29
|
caller?: string
|
|
30
30
|
): string {
|
|
31
31
|
const { scale, positionX, positionY, baseScale = 1 } = options;
|
|
32
|
+
|
|
33
|
+
// We combine baseScale (which brings image to min-cover size) with user's zoom scale
|
|
32
34
|
const totalScale = baseScale * scale;
|
|
33
35
|
|
|
34
|
-
//
|
|
36
|
+
// Center via translate -50% -50% (if top:50% left:50% is used)
|
|
35
37
|
const center = needsCentering ? 'translate(-50%, -50%)' : '';
|
|
36
38
|
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
const zoom = `scale(${totalScale})`;
|
|
40
|
-
|
|
41
|
-
// 3. Apply the offset (positionX/Y) AFTER scaling
|
|
42
|
-
// Position values are stored as percentage of CONTAINER
|
|
43
|
-
// Since scale is applied first, the translate is relative to the scaled visual size
|
|
39
|
+
// Position offset (stored as % of container)
|
|
40
|
+
// We apply this AFTER scaling so it moves relative to the scaled visual size
|
|
44
41
|
const offset = `translate(${positionX}%, ${positionY}%)`;
|
|
45
|
-
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
return `${center} ${
|
|
42
|
+
|
|
43
|
+
// Order: Center -> Move -> Scale
|
|
44
|
+
// This ensures position is relative to visual container space
|
|
45
|
+
return `${center} ${offset} scale(${totalScale})`.trim();
|
|
49
46
|
}
|
|
50
47
|
|
|
51
48
|
export function getImageFilter(brightness: number = 100, blur: number = 0): string | undefined {
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { X, Zap, Loader2 } from 'lucide-react';
|
|
5
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
6
|
+
import { StorageStats } from '../types';
|
|
7
|
+
|
|
8
|
+
interface CleanupLibraryModalProps {
|
|
9
|
+
isOpen: boolean;
|
|
10
|
+
stats: StorageStats | null;
|
|
11
|
+
unusedCount: number;
|
|
12
|
+
isCleaning: boolean;
|
|
13
|
+
onClose: () => void;
|
|
14
|
+
onConfirm: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function CleanupLibraryModal({
|
|
18
|
+
isOpen,
|
|
19
|
+
stats,
|
|
20
|
+
unusedCount,
|
|
21
|
+
isCleaning,
|
|
22
|
+
onClose,
|
|
23
|
+
onConfirm
|
|
24
|
+
}: CleanupLibraryModalProps) {
|
|
25
|
+
return (
|
|
26
|
+
<AnimatePresence>
|
|
27
|
+
{isOpen && (
|
|
28
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-6 bg-black/60 backdrop-blur-md">
|
|
29
|
+
<motion.div
|
|
30
|
+
initial={{ opacity: 0, scale: 0.9, y: 20 }}
|
|
31
|
+
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
32
|
+
exit={{ opacity: 0, scale: 0.9, y: 20 }}
|
|
33
|
+
className="glass-panel w-full max-w-lg rounded-[3rem] shadow-2xl overflow-hidden flex flex-col border border-white/10 dark:border-black/10"
|
|
34
|
+
>
|
|
35
|
+
<div className="p-10 pb-6 flex justify-between items-center shrink-0">
|
|
36
|
+
<div>
|
|
37
|
+
<h2 className="text-3xl font-black text-dashboard-text uppercase tracking-tighter leading-none mb-2">Library Optimization</h2>
|
|
38
|
+
<p className="text-[10px] font-black text-amber-500 uppercase tracking-[0.2em]">Maintenance Mode</p>
|
|
39
|
+
</div>
|
|
40
|
+
<button
|
|
41
|
+
onClick={onClose}
|
|
42
|
+
className="hover:rotate-90 hover:text-red-500 transition-all p-3 bg-dashboard-bg rounded-2xl border border-dashboard-border active:scale-90"
|
|
43
|
+
>
|
|
44
|
+
<X size={24} />
|
|
45
|
+
</button>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div className="px-10 pb-10 space-y-8">
|
|
49
|
+
<div className="flex items-center gap-4 p-6 bg-amber-500/5 border border-amber-500/20 rounded-3xl">
|
|
50
|
+
<Zap className="text-amber-500 shrink-0" size={32} />
|
|
51
|
+
<div className="min-w-0">
|
|
52
|
+
<p className="text-sm font-black text-amber-500 uppercase tracking-tight">Mass Deletion Warning</p>
|
|
53
|
+
<p className="text-xs text-dashboard-text-secondary font-medium mt-1 leading-relaxed italic">
|
|
54
|
+
{unusedCount} standalone assets will be purged from the cloud repository.
|
|
55
|
+
</p>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<div className="p-8 bg-dashboard-bg/50 border border-dashboard-border rounded-[2rem] space-y-4">
|
|
60
|
+
<div className="flex items-center justify-between">
|
|
61
|
+
<span className="text-[10px] font-black text-dashboard-text-secondary uppercase tracking-widest opacity-60">Repository Total</span>
|
|
62
|
+
<span className="text-sm font-black text-dashboard-text">{stats?.totalImages || 0}</span>
|
|
63
|
+
</div>
|
|
64
|
+
<div className="flex items-center justify-between border-t border-dashboard-border/30 pt-4">
|
|
65
|
+
<span className="text-[10px] font-black text-primary uppercase tracking-widest">Active Assets</span>
|
|
66
|
+
<span className="text-sm font-black text-primary">{stats?.usedImages || 0}</span>
|
|
67
|
+
</div>
|
|
68
|
+
<div className="flex items-center justify-between border-t border-dashboard-border/30 pt-4">
|
|
69
|
+
<span className="text-[10px] font-black text-red-500 uppercase tracking-widest">Purge Bundle</span>
|
|
70
|
+
<span className="text-sm font-black text-red-500">{unusedCount}</span>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<div className="flex gap-4 pt-4">
|
|
75
|
+
<button
|
|
76
|
+
onClick={onClose}
|
|
77
|
+
className="flex-1 py-4 bg-dashboard-bg text-dashboard-text-secondary border border-dashboard-border rounded-2xl text-[10px] font-black uppercase tracking-[0.2em] hover:bg-dashboard-card transition-all active:scale-[0.98]"
|
|
78
|
+
>
|
|
79
|
+
Abort
|
|
80
|
+
</button>
|
|
81
|
+
<button
|
|
82
|
+
onClick={onConfirm}
|
|
83
|
+
disabled={isCleaning}
|
|
84
|
+
className="flex-1 py-4 bg-red-500 text-white rounded-2xl text-[10px] font-black uppercase tracking-[0.2em] shadow-xl shadow-red-500/20 hover:bg-red-600 transition-all active:scale-[0.98] flex items-center justify-center gap-3"
|
|
85
|
+
>
|
|
86
|
+
{isCleaning && <Loader2 size={16} className="animate-spin" />}
|
|
87
|
+
Initialize Purge
|
|
88
|
+
</button>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</motion.div>
|
|
92
|
+
</div>
|
|
93
|
+
)}
|
|
94
|
+
</AnimatePresence>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import Image from 'next/image';
|
|
5
|
+
import { X, AlertTriangle, ExternalLink, Loader2 } from 'lucide-react';
|
|
6
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
7
|
+
import { ImageMetadata } from '../types';
|
|
8
|
+
|
|
9
|
+
interface DeleteImageModalProps {
|
|
10
|
+
isOpen: boolean;
|
|
11
|
+
image: ImageMetadata | null;
|
|
12
|
+
inUse: boolean;
|
|
13
|
+
usage: any[];
|
|
14
|
+
isDeleting: boolean;
|
|
15
|
+
formatFileSize: (bytes: number) => string;
|
|
16
|
+
formatDate: (date: string) => string;
|
|
17
|
+
onClose: () => void;
|
|
18
|
+
onConfirm: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function DeleteImageModal({
|
|
22
|
+
isOpen,
|
|
23
|
+
image,
|
|
24
|
+
inUse,
|
|
25
|
+
usage,
|
|
26
|
+
isDeleting,
|
|
27
|
+
formatFileSize,
|
|
28
|
+
formatDate,
|
|
29
|
+
onClose,
|
|
30
|
+
onConfirm
|
|
31
|
+
}: DeleteImageModalProps) {
|
|
32
|
+
return (
|
|
33
|
+
<AnimatePresence>
|
|
34
|
+
{isOpen && image && (
|
|
35
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-6 bg-black/60 backdrop-blur-md">
|
|
36
|
+
<motion.div
|
|
37
|
+
initial={{ opacity: 0, scale: 0.9, y: 20 }}
|
|
38
|
+
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
39
|
+
exit={{ opacity: 0, scale: 0.9, y: 20 }}
|
|
40
|
+
className="glass-panel w-full max-w-lg rounded-[3rem] shadow-2xl overflow-hidden flex flex-col border border-white/10 dark:border-black/10"
|
|
41
|
+
>
|
|
42
|
+
<div className="p-10 pb-6 flex justify-between items-center shrink-0">
|
|
43
|
+
<div>
|
|
44
|
+
<h2 className="text-3xl font-black text-dashboard-text uppercase tracking-tighter leading-none mb-2">Purge Asset</h2>
|
|
45
|
+
<p className="text-[10px] font-black text-red-500 uppercase tracking-[0.2em]">Destructive Action</p>
|
|
46
|
+
</div>
|
|
47
|
+
<button
|
|
48
|
+
onClick={onClose}
|
|
49
|
+
className="hover:rotate-90 hover:text-red-500 transition-all p-3 bg-dashboard-bg rounded-2xl border border-dashboard-border active:scale-90"
|
|
50
|
+
>
|
|
51
|
+
<X size={24} />
|
|
52
|
+
</button>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div className="px-10 pb-10 space-y-8">
|
|
56
|
+
{inUse ? (
|
|
57
|
+
<div className="space-y-6">
|
|
58
|
+
<div className="flex items-center gap-4 p-6 bg-red-500/5 border border-red-500/20 rounded-3xl">
|
|
59
|
+
<AlertTriangle className="text-red-500 shrink-0" size={32} />
|
|
60
|
+
<div>
|
|
61
|
+
<p className="text-sm font-black text-red-500 uppercase tracking-tight">System Integrity Violation</p>
|
|
62
|
+
<p className="text-xs text-dashboard-text-secondary font-medium mt-1 leading-relaxed italic">
|
|
63
|
+
This asset is actively connected to content and cannot be purged while linked.
|
|
64
|
+
</p>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
{(usage.length > 0) && (
|
|
69
|
+
<div className="space-y-4">
|
|
70
|
+
<p className="text-[10px] font-black text-dashboard-text-secondary uppercase tracking-[0.2em] ml-1">Active Connections:</p>
|
|
71
|
+
<div className="space-y-3 max-h-[250px] overflow-y-auto pr-2 custom-scrollbar">
|
|
72
|
+
{usage.map((item, idx) => (
|
|
73
|
+
<div key={idx} className="flex items-center justify-between p-4 bg-dashboard-bg/50 border border-dashboard-border rounded-2xl group hover:border-primary/30 transition-all">
|
|
74
|
+
<div>
|
|
75
|
+
<p className="text-xs font-black text-dashboard-text uppercase tracking-tight">{item.title}</p>
|
|
76
|
+
<p className="text-[9px] font-bold text-dashboard-text-secondary uppercase tracking-widest opacity-60 mt-0.5">{item.plugin} • {item.type}</p>
|
|
77
|
+
</div>
|
|
78
|
+
{item.url && (
|
|
79
|
+
<a
|
|
80
|
+
href={item.url}
|
|
81
|
+
target="_blank"
|
|
82
|
+
rel="noopener noreferrer"
|
|
83
|
+
className="p-3 bg-dashboard-card text-primary rounded-xl border border-dashboard-border group-hover:bg-primary group-hover:text-white transition-all shadow-sm"
|
|
84
|
+
>
|
|
85
|
+
<ExternalLink size={14} />
|
|
86
|
+
</a>
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
89
|
+
))}
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
)}
|
|
93
|
+
</div>
|
|
94
|
+
) : (
|
|
95
|
+
<div className="space-y-8">
|
|
96
|
+
<div className="flex items-center gap-6 p-6 bg-dashboard-bg/50 border border-dashboard-border rounded-[2rem]">
|
|
97
|
+
<div className="size-24 relative bg-dashboard-card rounded-2xl overflow-hidden border border-dashboard-border shadow-inner">
|
|
98
|
+
<Image
|
|
99
|
+
src={image.url}
|
|
100
|
+
alt={image.filename}
|
|
101
|
+
fill
|
|
102
|
+
className="object-cover"
|
|
103
|
+
unoptimized
|
|
104
|
+
/>
|
|
105
|
+
</div>
|
|
106
|
+
<div className="min-w-0">
|
|
107
|
+
<p className="text-lg font-black text-dashboard-text uppercase tracking-tight truncate">{image.filename}</p>
|
|
108
|
+
<div className="flex flex-col gap-1 mt-1">
|
|
109
|
+
<span className="text-[10px] font-bold text-dashboard-text-secondary uppercase tracking-widest opacity-60">{formatFileSize(image.size)}</span>
|
|
110
|
+
<span className="text-[10px] font-bold text-dashboard-text-secondary uppercase tracking-widest opacity-60">{formatDate(image.uploadedAt)}</span>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
<p className="text-sm text-dashboard-text-secondary font-medium leading-relaxed text-center italic px-4 opacity-70">
|
|
115
|
+
Are you certain you wish to permanently purge this asset from the repository? This operation is irreversible.
|
|
116
|
+
</p>
|
|
117
|
+
</div>
|
|
118
|
+
)}
|
|
119
|
+
|
|
120
|
+
<div className="flex gap-4 pt-4 border-t border-dashboard-border/50">
|
|
121
|
+
<button
|
|
122
|
+
onClick={onClose}
|
|
123
|
+
className="flex-1 py-4 bg-dashboard-bg text-dashboard-text-secondary border border-dashboard-border rounded-2xl text-[10px] font-black uppercase tracking-[0.2em] hover:bg-dashboard-card hover:text-dashboard-text transition-all active:scale-[0.98]"
|
|
124
|
+
>
|
|
125
|
+
Abort Protocol
|
|
126
|
+
</button>
|
|
127
|
+
{!inUse && (
|
|
128
|
+
<button
|
|
129
|
+
onClick={onConfirm}
|
|
130
|
+
disabled={isDeleting}
|
|
131
|
+
className="flex-1 py-4 bg-red-500 text-white rounded-2xl text-[10px] font-black uppercase tracking-[0.2em] shadow-xl shadow-red-500/20 hover:bg-red-600 transition-all active:scale-[0.98] flex items-center justify-center gap-3"
|
|
132
|
+
>
|
|
133
|
+
{isDeleting && <Loader2 size={16} className="animate-spin" />}
|
|
134
|
+
Confirm Purge
|
|
135
|
+
</button>
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
</motion.div>
|
|
140
|
+
</div>
|
|
141
|
+
)}
|
|
142
|
+
</AnimatePresence>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { Maximize2, Trash2, CheckCircle2 } from 'lucide-react';
|
|
5
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
6
|
+
import { ImageMetadata } from '../types';
|
|
7
|
+
|
|
8
|
+
interface ImageGridProps {
|
|
9
|
+
images: ImageMetadata[];
|
|
10
|
+
isImageInUse: (filename: string) => boolean;
|
|
11
|
+
getImageUsage: (filename: string) => any[];
|
|
12
|
+
formatFileSize: (bytes: number) => string;
|
|
13
|
+
onViewFull: (url: string) => void;
|
|
14
|
+
onDelete: (image: ImageMetadata) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function ImageGrid({
|
|
18
|
+
images,
|
|
19
|
+
isImageInUse,
|
|
20
|
+
getImageUsage,
|
|
21
|
+
formatFileSize,
|
|
22
|
+
onViewFull,
|
|
23
|
+
onDelete
|
|
24
|
+
}: ImageGridProps) {
|
|
25
|
+
return (
|
|
26
|
+
<div className="columns-2 sm:columns-3 lg:columns-4 xl:columns-5 gap-6 space-y-6">
|
|
27
|
+
<AnimatePresence mode="popLayout">
|
|
28
|
+
{images.map((image, index) => {
|
|
29
|
+
const usage = getImageUsage(image.filename);
|
|
30
|
+
const inUse = isImageInUse(image.filename);
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<motion.div
|
|
34
|
+
key={image.id}
|
|
35
|
+
initial={{ opacity: 0, y: 10 }}
|
|
36
|
+
animate={{ opacity: 1, y: 0 }}
|
|
37
|
+
exit={{ opacity: 0, scale: 0.9 }}
|
|
38
|
+
transition={{ duration: 0.3, delay: index * 0.02 }}
|
|
39
|
+
className="break-inside-avoid mb-6 group relative"
|
|
40
|
+
>
|
|
41
|
+
{/* Main Card Container */}
|
|
42
|
+
<div className={`relative bg-dashboard-card/40 rounded-2xl overflow-hidden border transition-all duration-500 shadow-sm group-hover:shadow-xl group-hover:shadow-primary/10 ${
|
|
43
|
+
inUse
|
|
44
|
+
? 'border-dashboard-border/40'
|
|
45
|
+
: 'border-dashboard-border/60 group-hover:border-primary/30'
|
|
46
|
+
}`}>
|
|
47
|
+
{/* The Image - Full Bleed */}
|
|
48
|
+
<img
|
|
49
|
+
src={image.url}
|
|
50
|
+
alt={image.filename}
|
|
51
|
+
className="w-full h-auto object-cover group-hover:scale-105 transition-transform duration-700 block"
|
|
52
|
+
crossOrigin="anonymous"
|
|
53
|
+
loading="lazy"
|
|
54
|
+
/>
|
|
55
|
+
|
|
56
|
+
{/* 1. Permanent Status Badge (Top Right) */}
|
|
57
|
+
{inUse && (
|
|
58
|
+
<div className="absolute top-3 right-3 px-2.5 py-1 bg-emerald-500/90 backdrop-blur-sm text-white text-[8px] font-bold uppercase tracking-wider rounded-full flex items-center gap-1.5 shadow-lg shadow-emerald-500/20 z-20">
|
|
59
|
+
<CheckCircle2 size={10} />
|
|
60
|
+
In Use
|
|
61
|
+
</div>
|
|
62
|
+
)}
|
|
63
|
+
|
|
64
|
+
{/* 2. SMART HOVER OVERLAY */}
|
|
65
|
+
<div className="absolute inset-0 z-10 opacity-0 group-hover:opacity-100 transition-all duration-300 flex flex-col justify-between p-5">
|
|
66
|
+
<div className="absolute inset-0 bg-gradient-to-b from-black/10 via-black/40 to-black/70 backdrop-blur-[1px]" />
|
|
67
|
+
|
|
68
|
+
{/* Action Buttons (Center) */}
|
|
69
|
+
<div className="relative z-20 flex-1 flex items-center justify-center gap-2.5 translate-y-2 group-hover:translate-y-0 transition-transform duration-300">
|
|
70
|
+
<button
|
|
71
|
+
onClick={() => onViewFull(image.url)}
|
|
72
|
+
className="p-3 bg-white text-primary rounded-xl hover:scale-105 active:scale-95 transition-all shadow-xl"
|
|
73
|
+
title="View Image"
|
|
74
|
+
>
|
|
75
|
+
<Maximize2 size={18} />
|
|
76
|
+
</button>
|
|
77
|
+
<button
|
|
78
|
+
onClick={() => onDelete(image)}
|
|
79
|
+
className={`p-3 rounded-xl shadow-xl transition-all hover:scale-105 active:scale-95 ${
|
|
80
|
+
inUse
|
|
81
|
+
? 'bg-white/10 text-white/30 cursor-not-allowed'
|
|
82
|
+
: 'bg-red-500 text-white hover:bg-red-600'
|
|
83
|
+
}`}
|
|
84
|
+
title={inUse ? 'Image is in use' : 'Delete Image'}
|
|
85
|
+
disabled={inUse}
|
|
86
|
+
>
|
|
87
|
+
<Trash2 size={18} />
|
|
88
|
+
</button>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
{/* Metadata Footer (Bottom) */}
|
|
92
|
+
<div className="relative z-20 space-y-1.5 translate-y-2 group-hover:translate-y-0 transition-transform duration-300 delay-75">
|
|
93
|
+
<p className="text-[11px] font-bold text-white tracking-tight truncate w-full" title={image.filename}>
|
|
94
|
+
{image.filename}
|
|
95
|
+
</p>
|
|
96
|
+
<div className="flex items-center justify-between border-t border-white/10 pt-1.5">
|
|
97
|
+
<span className="text-[9px] font-bold text-white/60 uppercase tracking-widest">
|
|
98
|
+
{formatFileSize(image.size)}
|
|
99
|
+
</span>
|
|
100
|
+
{inUse ? (
|
|
101
|
+
<span className="text-[9px] text-emerald-400 font-bold uppercase">
|
|
102
|
+
{usage.length > 0 ? `${usage.length} Uses` : 'Active'}
|
|
103
|
+
</span>
|
|
104
|
+
) : (
|
|
105
|
+
<span className="text-[9px] text-white/40 font-bold uppercase">
|
|
106
|
+
Available
|
|
107
|
+
</span>
|
|
108
|
+
)}
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
</motion.div>
|
|
114
|
+
);
|
|
115
|
+
})}
|
|
116
|
+
</AnimatePresence>
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { FileImage, Zap, Loader2, Upload, RefreshCw } from 'lucide-react';
|
|
5
|
+
|
|
6
|
+
interface ImageManagerHeaderProps {
|
|
7
|
+
uploading: boolean;
|
|
8
|
+
unusedCount: number;
|
|
9
|
+
onUpload: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
|
10
|
+
onRefresh: () => void;
|
|
11
|
+
onShowCleanup: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function ImageManagerHeader({
|
|
15
|
+
uploading,
|
|
16
|
+
unusedCount,
|
|
17
|
+
onUpload,
|
|
18
|
+
onRefresh,
|
|
19
|
+
onShowCleanup
|
|
20
|
+
}: ImageManagerHeaderProps) {
|
|
21
|
+
return (
|
|
22
|
+
<div className="flex flex-col lg:flex-row lg:items-end justify-between gap-8 px-4">
|
|
23
|
+
<div className="space-y-3">
|
|
24
|
+
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-primary/15 border border-primary/30 text-primary text-[10px] font-bold uppercase tracking-wider shadow-sm">
|
|
25
|
+
<FileImage size={12} />
|
|
26
|
+
<span>Media Library</span>
|
|
27
|
+
</div>
|
|
28
|
+
<div>
|
|
29
|
+
<h1 className="text-4xl font-bold text-dashboard-text tracking-tight leading-none mb-2">
|
|
30
|
+
Images <span className="text-primary">&</span> Media
|
|
31
|
+
</h1>
|
|
32
|
+
<p className="text-sm text-dashboard-text-secondary font-medium max-w-md leading-relaxed opacity-80">
|
|
33
|
+
Upload and manage your images. See exactly where they are used across your website.
|
|
34
|
+
</p>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div className="flex items-center gap-4">
|
|
39
|
+
{unusedCount > 0 && (
|
|
40
|
+
<button
|
|
41
|
+
onClick={onShowCleanup}
|
|
42
|
+
className="group flex items-center gap-3 px-6 py-3.5 bg-dashboard-card/50 border border-dashboard-border/40 text-amber-600 rounded-2xl text-[10px] font-bold uppercase tracking-widest transition-all hover:bg-amber-500/10 hover:border-amber-500/20 active:scale-95 shadow-sm"
|
|
43
|
+
>
|
|
44
|
+
<Zap size={16} />
|
|
45
|
+
Cleanup Library
|
|
46
|
+
</button>
|
|
47
|
+
)}
|
|
48
|
+
|
|
49
|
+
<label className="group relative flex items-center gap-3 px-7 py-3.5 bg-primary text-white rounded-2xl text-[10px] font-bold uppercase tracking-widest overflow-hidden transition-all hover:scale-[1.02] active:scale-95 shadow-lg shadow-primary/20 cursor-pointer">
|
|
50
|
+
{uploading ? <Loader2 size={18} className="relative z-10 animate-spin" /> : <Upload size={18} className="relative z-10" />}
|
|
51
|
+
<span className="relative z-10">{uploading ? 'Uploading...' : 'Upload Media'}</span>
|
|
52
|
+
<input
|
|
53
|
+
type="file"
|
|
54
|
+
accept="image/*"
|
|
55
|
+
multiple
|
|
56
|
+
onChange={onUpload}
|
|
57
|
+
className="hidden"
|
|
58
|
+
disabled={uploading}
|
|
59
|
+
/>
|
|
60
|
+
</label>
|
|
61
|
+
|
|
62
|
+
<button
|
|
63
|
+
onClick={onRefresh}
|
|
64
|
+
className="p-3.5 bg-dashboard-card/50 border border-dashboard-border/40 text-dashboard-text-secondary hover:text-primary hover:border-primary/30 transition-all rounded-2xl active:scale-95"
|
|
65
|
+
title="Refresh Gallery"
|
|
66
|
+
>
|
|
67
|
+
<RefreshCw size={18} />
|
|
68
|
+
</button>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { Image as ImageIcon, Database, CheckCircle2, Activity } from 'lucide-react';
|
|
5
|
+
import { StorageStats } from '../types';
|
|
6
|
+
|
|
7
|
+
interface ImageManagerStatsProps {
|
|
8
|
+
stats: StorageStats;
|
|
9
|
+
formatFileSize: (bytes: number) => string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function ImageManagerStats({ stats, formatFileSize }: ImageManagerStatsProps) {
|
|
13
|
+
return (
|
|
14
|
+
<div className="grid grid-cols-2 lg:grid-cols-4 gap-5 px-4">
|
|
15
|
+
<div className="bg-dashboard-card/50 border border-dashboard-border/40 p-5 rounded-2xl relative overflow-hidden group transition-all duration-500">
|
|
16
|
+
<div className="absolute top-0 right-0 p-4 opacity-5 group-hover:scale-105 group-hover:text-primary transition-all duration-700">
|
|
17
|
+
<ImageIcon size={80} />
|
|
18
|
+
</div>
|
|
19
|
+
<div className="relative z-10">
|
|
20
|
+
<label className="text-[10px] font-bold text-primary uppercase tracking-widest mb-1 block opacity-80">Total Assets</label>
|
|
21
|
+
<p className="text-2xl font-bold text-dashboard-text tracking-tight">{stats.totalImages}</p>
|
|
22
|
+
<p className="text-[9px] text-dashboard-text-secondary mt-1 font-semibold uppercase opacity-60">Optimized Library</p>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div className="bg-dashboard-card/50 border border-dashboard-border/40 p-5 rounded-2xl relative overflow-hidden group transition-all duration-500">
|
|
27
|
+
<div className="absolute top-0 right-0 p-4 opacity-5 group-hover:scale-105 group-hover:text-amber-500 transition-all duration-700">
|
|
28
|
+
<Database size={80} />
|
|
29
|
+
</div>
|
|
30
|
+
<div className="relative z-10">
|
|
31
|
+
<label className="text-[10px] font-bold text-amber-500 uppercase tracking-widest mb-1 block opacity-80">Storage Usage</label>
|
|
32
|
+
<p className="text-2xl font-bold text-dashboard-text tracking-tight">{formatFileSize(stats.totalSize)}</p>
|
|
33
|
+
<p className="text-[9px] text-dashboard-text-secondary mt-1 font-semibold uppercase opacity-60">Cloud-optimized Storage</p>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div className="bg-dashboard-card/50 border border-dashboard-border/40 p-5 rounded-2xl relative overflow-hidden group transition-all duration-500">
|
|
38
|
+
<div className="absolute top-0 right-0 p-4 opacity-5 group-hover:scale-105 group-hover:text-emerald-500 transition-all duration-700">
|
|
39
|
+
<CheckCircle2 size={80} />
|
|
40
|
+
</div>
|
|
41
|
+
<div className="relative z-10">
|
|
42
|
+
<label className="text-[10px] font-bold text-emerald-500 uppercase tracking-widest mb-1 block opacity-80">Active Images</label>
|
|
43
|
+
<p className="text-2xl font-bold text-dashboard-text tracking-tight">{stats.usedImages}</p>
|
|
44
|
+
<p className="text-[9px] text-dashboard-text-secondary mt-1 font-semibold uppercase opacity-60">Linked to Website</p>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div className="bg-dashboard-card/50 border border-dashboard-border/40 p-5 rounded-2xl relative overflow-hidden group transition-all duration-500">
|
|
49
|
+
<div className="absolute top-0 right-0 p-4 opacity-5 group-hover:scale-105 group-hover:text-neutral-500 transition-all duration-700">
|
|
50
|
+
<Activity size={80} />
|
|
51
|
+
</div>
|
|
52
|
+
<div className="relative z-10">
|
|
53
|
+
<label className="text-[10px] font-bold text-dashboard-text-secondary uppercase tracking-widest mb-1 block opacity-80">Unused Assets</label>
|
|
54
|
+
<p className="text-2xl font-bold text-dashboard-text tracking-tight">{stats.availableImages}</p>
|
|
55
|
+
<p className="text-[9px] text-dashboard-text-secondary mt-1 font-semibold uppercase opacity-60">Ready for Removal</p>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|