@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.
Files changed (94) hide show
  1. package/dist/components/Image.d.ts +1 -6
  2. package/dist/components/Image.d.ts.map +1 -1
  3. package/dist/components/Image.js +86 -202
  4. package/dist/components/ImageEditor.d.ts.map +1 -1
  5. package/dist/components/ImageEditor.js +21 -125
  6. package/dist/components/ImagePicker.d.ts.map +1 -1
  7. package/dist/components/ImagePicker.js +6 -59
  8. package/dist/utils/fallback.d.ts +9 -4
  9. package/dist/utils/fallback.d.ts.map +1 -1
  10. package/dist/utils/fallback.js +40 -12
  11. package/dist/utils/transforms.d.ts.map +1 -1
  12. package/dist/utils/transforms.js +7 -10
  13. package/dist/views/ImageManager/components/CleanupLibraryModal.d.ts +12 -0
  14. package/dist/views/ImageManager/components/CleanupLibraryModal.d.ts.map +1 -0
  15. package/dist/views/ImageManager/components/CleanupLibraryModal.js +7 -0
  16. package/dist/views/ImageManager/components/DeleteImageModal.d.ts +15 -0
  17. package/dist/views/ImageManager/components/DeleteImageModal.d.ts.map +1 -0
  18. package/dist/views/ImageManager/components/DeleteImageModal.js +8 -0
  19. package/dist/views/ImageManager/components/ImageGrid.d.ts +12 -0
  20. package/dist/views/ImageManager/components/ImageGrid.d.ts.map +1 -0
  21. package/dist/views/ImageManager/components/ImageGrid.js +15 -0
  22. package/dist/views/ImageManager/components/ImageManagerHeader.d.ts +11 -0
  23. package/dist/views/ImageManager/components/ImageManagerHeader.d.ts.map +1 -0
  24. package/dist/views/ImageManager/components/ImageManagerHeader.js +6 -0
  25. package/dist/views/ImageManager/components/ImageManagerStats.d.ts +8 -0
  26. package/dist/views/ImageManager/components/ImageManagerStats.d.ts.map +1 -0
  27. package/dist/views/ImageManager/components/ImageManagerStats.js +6 -0
  28. package/dist/views/ImageManager/components/ImageManagerToolbar.d.ts +9 -0
  29. package/dist/views/ImageManager/components/ImageManagerToolbar.d.ts.map +1 -0
  30. package/dist/views/ImageManager/components/ImageManagerToolbar.js +10 -0
  31. package/dist/views/ImageManager/components/ImageTable.d.ts +13 -0
  32. package/dist/views/ImageManager/components/ImageTable.d.ts.map +1 -0
  33. package/dist/views/ImageManager/components/ImageTable.js +13 -0
  34. package/dist/views/ImageManager/types.d.ts +26 -0
  35. package/dist/views/ImageManager/types.d.ts.map +1 -0
  36. package/dist/views/ImageManager/types.js +1 -0
  37. package/dist/views/ImageManager.d.ts +1 -1
  38. package/dist/views/ImageManager.d.ts.map +1 -1
  39. package/dist/views/ImageManager.js +28 -52
  40. package/package.json +10 -9
  41. package/src/components/Image.tsx +107 -262
  42. package/src/components/ImageEditor.tsx +31 -193
  43. package/src/components/ImagePicker.tsx +22 -107
  44. package/src/utils/fallback.ts +46 -13
  45. package/src/utils/transforms.ts +9 -12
  46. package/src/views/ImageManager/components/CleanupLibraryModal.tsx +96 -0
  47. package/src/views/ImageManager/components/DeleteImageModal.tsx +144 -0
  48. package/src/views/ImageManager/components/ImageGrid.tsx +119 -0
  49. package/src/views/ImageManager/components/ImageManagerHeader.tsx +72 -0
  50. package/src/views/ImageManager/components/ImageManagerStats.tsx +60 -0
  51. package/src/views/ImageManager/components/ImageManagerToolbar.tsx +60 -0
  52. package/src/views/ImageManager/components/ImageTable.tsx +120 -0
  53. package/src/views/ImageManager/types.ts +27 -0
  54. package/src/views/ImageManager.tsx +103 -571
  55. package/src/components/BackgroundImage.d.ts +0 -11
  56. package/src/components/BackgroundImage.d.ts.map +0 -1
  57. package/src/components/GlobalImageEditor/config.d.ts +0 -9
  58. package/src/components/GlobalImageEditor/config.d.ts.map +0 -1
  59. package/src/components/GlobalImageEditor/eventHandlers.d.ts +0 -20
  60. package/src/components/GlobalImageEditor/eventHandlers.d.ts.map +0 -1
  61. package/src/components/GlobalImageEditor/imageDetection.d.ts +0 -16
  62. package/src/components/GlobalImageEditor/imageDetection.d.ts.map +0 -1
  63. package/src/components/GlobalImageEditor/imageSetup.d.ts +0 -9
  64. package/src/components/GlobalImageEditor/imageSetup.d.ts.map +0 -1
  65. package/src/components/GlobalImageEditor/saveLogic.d.ts +0 -26
  66. package/src/components/GlobalImageEditor/saveLogic.d.ts.map +0 -1
  67. package/src/components/GlobalImageEditor/stylingDetection.d.ts +0 -9
  68. package/src/components/GlobalImageEditor/stylingDetection.d.ts.map +0 -1
  69. package/src/components/GlobalImageEditor/transformParsing.d.ts +0 -16
  70. package/src/components/GlobalImageEditor/transformParsing.d.ts.map +0 -1
  71. package/src/components/GlobalImageEditor/types.d.ts +0 -36
  72. package/src/components/GlobalImageEditor/types.d.ts.map +0 -1
  73. package/src/components/GlobalImageEditor.d.ts +0 -8
  74. package/src/components/GlobalImageEditor.d.ts.map +0 -1
  75. package/src/components/Image.d.ts +0 -22
  76. package/src/components/Image.d.ts.map +0 -1
  77. package/src/components/ImageBrowserModal.d.ts +0 -13
  78. package/src/components/ImageBrowserModal.d.ts.map +0 -1
  79. package/src/components/ImageEditor.d.ts +0 -27
  80. package/src/components/ImageEditor.d.ts.map +0 -1
  81. package/src/components/ImagePicker.d.ts +0 -3
  82. package/src/components/ImagePicker.d.ts.map +0 -1
  83. package/src/components/ImagesPluginInit.d.ts +0 -24
  84. package/src/components/ImagesPluginInit.d.ts.map +0 -1
  85. package/src/hooks/useImagePicker.d.ts +0 -20
  86. package/src/hooks/useImagePicker.d.ts.map +0 -1
  87. package/src/types/index.d.ts +0 -80
  88. package/src/types/index.d.ts.map +0 -1
  89. package/src/utils/fallback.d.ts +0 -27
  90. package/src/utils/fallback.d.ts.map +0 -1
  91. package/src/utils/transforms.d.ts +0 -26
  92. package/src/utils/transforms.d.ts.map +0 -1
  93. package/src/views/ImageManager.d.ts +0 -10
  94. package/src/views/ImageManager.d.ts.map +0 -1
@@ -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
- * Served from the plugin's API route
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 '/api/plugin-images/fallback';
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 `/api/uploads/${filename}`
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 a full URL (absolute or relative), return as-is
26
- if (src.startsWith('http://') || src.startsWith('https://') || src.startsWith('/')) {
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
- // Otherwise, it's a filename - construct the API URL
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
- * Validates if a URL is valid and can be used with Next.js Image component
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
- // For relative URLs (starting with /), they're valid
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
- // For absolute URLs, validate the URL format
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
-
@@ -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
- // 1. Center the image (if using top:50% left:50%)
36
+ // Center via translate -50% -50% (if top:50% left:50% is used)
35
37
  const center = needsCentering ? 'translate(-50%, -50%)' : '';
36
38
 
37
- // 2. Apply the scaling (base x zoom) FIRST
38
- // This ensures the position offset is relative to the scaled visual size
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
- // Combining them: Center first, then scale, then move
47
- // Order matters: scale before translate ensures position is relative to visual size
48
- return `${center} ${zoom} ${offset}`.trim();
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
+ }