@mgonza02/goal-image-gallery 1.0.0
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/IMPLEMENTATION_SUMMARY.md +259 -0
- package/LICENSE +21 -0
- package/MIGRATION.md +428 -0
- package/QUICK_REFERENCE.md +235 -0
- package/README.md +479 -0
- package/lib/GoalImageGallery.js +244 -0
- package/lib/components/ImageGalleryEmptyState.js +87 -0
- package/lib/components/ImageGalleryGrid.js +108 -0
- package/lib/components/ImageGalleryItem.js +175 -0
- package/lib/components/ImageGalleryModal.js +149 -0
- package/lib/components/index.js +5 -0
- package/lib/examples.js +503 -0
- package/lib/hooks/use-image-gallery.js +252 -0
- package/lib/index.d.ts +187 -0
- package/lib/index.js +148 -0
- package/lib/styles/styled-components.js +173 -0
- package/lib/utils/image-utils.js +129 -0
- package/package.json +93 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { useEffect, useState, useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Custom hook for managing image gallery state
|
|
5
|
+
*/
|
|
6
|
+
export const useImageGallery = _ref => {
|
|
7
|
+
let {
|
|
8
|
+
imageCodes,
|
|
9
|
+
multiple,
|
|
10
|
+
canEdit,
|
|
11
|
+
updatable,
|
|
12
|
+
imageHandlerApi,
|
|
13
|
+
ownerEntity,
|
|
14
|
+
currentCompany,
|
|
15
|
+
onRefresh,
|
|
16
|
+
showError
|
|
17
|
+
} = _ref;
|
|
18
|
+
const [openImage, setOpenImage] = useState(null);
|
|
19
|
+
const [selectedImageIndex, setSelectedImageIndex] = useState(0);
|
|
20
|
+
const [selectingImage, setSelectingImage] = useState(false);
|
|
21
|
+
const [imageList, setImageList] = useState([]);
|
|
22
|
+
const [selectedImage, setSelectedImage] = useState(null);
|
|
23
|
+
const [zoomLevel, setZoomLevel] = useState(1);
|
|
24
|
+
const [loading, setLoading] = useState(false);
|
|
25
|
+
const [removable, setRemovable] = useState(false);
|
|
26
|
+
|
|
27
|
+
// Process image codes into image list
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const splitedImages = Array.isArray(imageCodes) ? imageCodes : imageCodes?.split(/[\s,]+/);
|
|
30
|
+
let newList = [];
|
|
31
|
+
if (splitedImages) {
|
|
32
|
+
const list = splitedImages.map((image_code, index) => ({
|
|
33
|
+
id: index,
|
|
34
|
+
width: 1,
|
|
35
|
+
height: 1,
|
|
36
|
+
title: 'Goal Image',
|
|
37
|
+
image_code
|
|
38
|
+
}));
|
|
39
|
+
newList = [...list];
|
|
40
|
+
} else {
|
|
41
|
+
newList = [];
|
|
42
|
+
}
|
|
43
|
+
setRemovable(multiple && newList.length > 1);
|
|
44
|
+
|
|
45
|
+
// Add plus button for multiple mode
|
|
46
|
+
if (updatable && multiple) {
|
|
47
|
+
newList.push({
|
|
48
|
+
image_code: '+',
|
|
49
|
+
id: newList.length
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
setImageList(newList);
|
|
53
|
+
}, [imageCodes, canEdit, multiple, updatable]);
|
|
54
|
+
|
|
55
|
+
// Image handlers
|
|
56
|
+
const handleOpenImage = useCallback(function (imageUrl) {
|
|
57
|
+
let index = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
|
|
58
|
+
setOpenImage(imageUrl);
|
|
59
|
+
setSelectedImageIndex(index);
|
|
60
|
+
setZoomLevel(1);
|
|
61
|
+
}, []);
|
|
62
|
+
const handleCloseImage = useCallback(() => {
|
|
63
|
+
setOpenImage(null);
|
|
64
|
+
setZoomLevel(1);
|
|
65
|
+
}, []);
|
|
66
|
+
const handleImageSelect = useCallback(image => {
|
|
67
|
+
setSelectedImage(image);
|
|
68
|
+
setSelectingImage(true);
|
|
69
|
+
}, []);
|
|
70
|
+
const handleCancel = useCallback(() => {
|
|
71
|
+
setSelectingImage(false);
|
|
72
|
+
}, []);
|
|
73
|
+
const handleRemoveImage = useCallback(async image => {
|
|
74
|
+
setLoading(true);
|
|
75
|
+
try {
|
|
76
|
+
const imagesToSave = imageList.reduce((acc, img) => {
|
|
77
|
+
if (img.image_code !== '+' && img.image_code !== image.image_code) {
|
|
78
|
+
acc.push(img.image_code);
|
|
79
|
+
}
|
|
80
|
+
return acc;
|
|
81
|
+
}, []);
|
|
82
|
+
const result = await imageHandlerApi({
|
|
83
|
+
...ownerEntity,
|
|
84
|
+
companyId: currentCompany,
|
|
85
|
+
image_code: imagesToSave,
|
|
86
|
+
company_id: currentCompany
|
|
87
|
+
});
|
|
88
|
+
if (result.success) {
|
|
89
|
+
onRefresh?.(result);
|
|
90
|
+
} else {
|
|
91
|
+
showError(result.message);
|
|
92
|
+
}
|
|
93
|
+
} catch (error) {
|
|
94
|
+
showError(error.message || 'Error al eliminar la imagen');
|
|
95
|
+
} finally {
|
|
96
|
+
setLoading(false);
|
|
97
|
+
}
|
|
98
|
+
}, [ownerEntity, currentCompany, imageList, onRefresh, showError, imageHandlerApi]);
|
|
99
|
+
const handleAfterUpload = useCallback(async image => {
|
|
100
|
+
const currentImageCode = selectedImage?.image_code || '';
|
|
101
|
+
const imagesToSave = imageList.reduce((acc, img) => {
|
|
102
|
+
if (img.image_code !== '+') {
|
|
103
|
+
acc.push(img.image_code === currentImageCode ? image.code : img.image_code);
|
|
104
|
+
}
|
|
105
|
+
return acc;
|
|
106
|
+
}, []);
|
|
107
|
+
if (!currentImageCode) {
|
|
108
|
+
imagesToSave.push(image.code);
|
|
109
|
+
}
|
|
110
|
+
const result = await imageHandlerApi({
|
|
111
|
+
...ownerEntity,
|
|
112
|
+
companyId: currentCompany,
|
|
113
|
+
image_code: imagesToSave,
|
|
114
|
+
company_id: currentCompany
|
|
115
|
+
});
|
|
116
|
+
if (result.success) {
|
|
117
|
+
setSelectingImage(false);
|
|
118
|
+
onRefresh?.(result);
|
|
119
|
+
} else {
|
|
120
|
+
showError(result.message);
|
|
121
|
+
}
|
|
122
|
+
}, [ownerEntity, currentCompany, imageList, selectedImage, imageHandlerApi, onRefresh, showError]);
|
|
123
|
+
return {
|
|
124
|
+
openImage,
|
|
125
|
+
selectedImageIndex,
|
|
126
|
+
selectingImage,
|
|
127
|
+
imageList,
|
|
128
|
+
selectedImage,
|
|
129
|
+
zoomLevel,
|
|
130
|
+
loading,
|
|
131
|
+
removable,
|
|
132
|
+
setZoomLevel,
|
|
133
|
+
handleOpenImage,
|
|
134
|
+
handleCloseImage,
|
|
135
|
+
handleImageSelect,
|
|
136
|
+
handleCancel,
|
|
137
|
+
handleRemoveImage,
|
|
138
|
+
handleAfterUpload
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Custom hook for managing image zoom functionality
|
|
144
|
+
*/
|
|
145
|
+
export const useImageZoom = (getImageUrl, imageList) => {
|
|
146
|
+
const [zoomLevel, setZoomLevel] = useState(1);
|
|
147
|
+
const [selectedImageIndex, setSelectedImageIndex] = useState(0);
|
|
148
|
+
const handleZoomIn = useCallback(() => {
|
|
149
|
+
setZoomLevel(prev => Math.min(prev + 0.5, 3));
|
|
150
|
+
}, []);
|
|
151
|
+
const handleZoomOut = useCallback(() => {
|
|
152
|
+
setZoomLevel(prev => Math.max(prev - 0.5, 0.5));
|
|
153
|
+
}, []);
|
|
154
|
+
const handlePreviousImage = useCallback(() => {
|
|
155
|
+
const newIndex = selectedImageIndex > 0 ? selectedImageIndex - 1 : imageList.length - 1;
|
|
156
|
+
setSelectedImageIndex(newIndex);
|
|
157
|
+
setZoomLevel(1);
|
|
158
|
+
return getImageUrl({
|
|
159
|
+
imageCode: imageList[newIndex].image_code
|
|
160
|
+
});
|
|
161
|
+
}, [selectedImageIndex, imageList, getImageUrl]);
|
|
162
|
+
const handleNextImage = useCallback(() => {
|
|
163
|
+
const newIndex = selectedImageIndex < imageList.length - 1 ? selectedImageIndex + 1 : 0;
|
|
164
|
+
setSelectedImageIndex(newIndex);
|
|
165
|
+
setZoomLevel(1);
|
|
166
|
+
return getImageUrl({
|
|
167
|
+
imageCode: imageList[newIndex].image_code
|
|
168
|
+
});
|
|
169
|
+
}, [selectedImageIndex, imageList, getImageUrl]);
|
|
170
|
+
return {
|
|
171
|
+
zoomLevel,
|
|
172
|
+
selectedImageIndex,
|
|
173
|
+
setZoomLevel,
|
|
174
|
+
setSelectedImageIndex,
|
|
175
|
+
handleZoomIn,
|
|
176
|
+
handleZoomOut,
|
|
177
|
+
handlePreviousImage,
|
|
178
|
+
handleNextImage
|
|
179
|
+
};
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Custom hook for clipboard functionality
|
|
184
|
+
*/
|
|
185
|
+
export const useClipboard = (updatable, selectingImage, onPasteImage) => {
|
|
186
|
+
const [clipboardSupported, setClipboardSupported] = useState(false);
|
|
187
|
+
useEffect(() => {
|
|
188
|
+
setClipboardSupported(navigator.clipboard);
|
|
189
|
+
}, []);
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
const handlePaste = async event => {
|
|
192
|
+
if (!clipboardSupported || !updatable || selectingImage) return;
|
|
193
|
+
const items = event.clipboardData?.items;
|
|
194
|
+
if (!items) return;
|
|
195
|
+
for (let i = 0; i < items.length; i++) {
|
|
196
|
+
const item = items[i];
|
|
197
|
+
if (item.type.indexOf('image') !== -1) {
|
|
198
|
+
const file = item.getAsFile();
|
|
199
|
+
if (file) {
|
|
200
|
+
onPasteImage?.(file);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
if (clipboardSupported && updatable) {
|
|
206
|
+
document.addEventListener('paste', handlePaste);
|
|
207
|
+
return () => document.removeEventListener('paste', handlePaste);
|
|
208
|
+
}
|
|
209
|
+
}, [clipboardSupported, updatable, selectingImage, onPasteImage]);
|
|
210
|
+
return {
|
|
211
|
+
clipboardSupported
|
|
212
|
+
};
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Custom hook for keyboard navigation
|
|
217
|
+
*/
|
|
218
|
+
export const useKeyboardNavigation = (openImage, onPreviousImage, onNextImage, onCloseImage, onZoomIn, onZoomOut) => {
|
|
219
|
+
useEffect(() => {
|
|
220
|
+
const handleKeyDown = event => {
|
|
221
|
+
if (openImage !== null) {
|
|
222
|
+
switch (event.key) {
|
|
223
|
+
case 'ArrowLeft':
|
|
224
|
+
event.preventDefault();
|
|
225
|
+
onPreviousImage();
|
|
226
|
+
break;
|
|
227
|
+
case 'ArrowRight':
|
|
228
|
+
event.preventDefault();
|
|
229
|
+
onNextImage();
|
|
230
|
+
break;
|
|
231
|
+
case 'Escape':
|
|
232
|
+
event.preventDefault();
|
|
233
|
+
onCloseImage();
|
|
234
|
+
break;
|
|
235
|
+
case '+':
|
|
236
|
+
case '=':
|
|
237
|
+
event.preventDefault();
|
|
238
|
+
onZoomIn();
|
|
239
|
+
break;
|
|
240
|
+
case '-':
|
|
241
|
+
event.preventDefault();
|
|
242
|
+
onZoomOut();
|
|
243
|
+
break;
|
|
244
|
+
default:
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
250
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
251
|
+
}, [openImage, onPreviousImage, onNextImage, onCloseImage, onZoomIn, onZoomOut]);
|
|
252
|
+
};
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
// Main component props
|
|
4
|
+
export interface GoalImageGalleryProps {
|
|
5
|
+
canEdit?: boolean;
|
|
6
|
+
ownerEntity?: object;
|
|
7
|
+
imageHandlerApi?: (data: any) => Promise<any>;
|
|
8
|
+
afterUpload?: (result: any) => void;
|
|
9
|
+
imageCodes?: string | string[];
|
|
10
|
+
multiple?: boolean;
|
|
11
|
+
permission?: string;
|
|
12
|
+
emptyMessage?: string;
|
|
13
|
+
showImageInfo?: boolean;
|
|
14
|
+
allowDownload?: boolean;
|
|
15
|
+
getImageUrl?: (params: { imageCode: string; thumbMail?: boolean }) => string;
|
|
16
|
+
noImageUrl?: string;
|
|
17
|
+
showError?: (message: string) => void;
|
|
18
|
+
hasPermission?: (permission: string) => boolean;
|
|
19
|
+
currentCompany?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Image item interface
|
|
23
|
+
export interface ImageItem {
|
|
24
|
+
id: number;
|
|
25
|
+
width: number;
|
|
26
|
+
height: number;
|
|
27
|
+
title: string;
|
|
28
|
+
image_code: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Hook interfaces
|
|
32
|
+
export interface UseImageGalleryOptions {
|
|
33
|
+
imageCodes?: string | string[];
|
|
34
|
+
multiple?: boolean;
|
|
35
|
+
canEdit?: boolean;
|
|
36
|
+
updatable?: boolean;
|
|
37
|
+
imageHandlerApi?: (data: any) => Promise<any>;
|
|
38
|
+
ownerEntity?: object;
|
|
39
|
+
currentCompany?: string;
|
|
40
|
+
onRefresh?: (result: any) => void;
|
|
41
|
+
showError?: (message: string) => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface UseImageGalleryReturn {
|
|
45
|
+
openImage: string | null;
|
|
46
|
+
selectedImageIndex: number;
|
|
47
|
+
selectingImage: boolean;
|
|
48
|
+
imageList: ImageItem[];
|
|
49
|
+
selectedImage: ImageItem | null;
|
|
50
|
+
zoomLevel: number;
|
|
51
|
+
loading: boolean;
|
|
52
|
+
removable: boolean;
|
|
53
|
+
setZoomLevel: (level: number) => void;
|
|
54
|
+
handleOpenImage: (imageUrl: string, index?: number) => void;
|
|
55
|
+
handleCloseImage: () => void;
|
|
56
|
+
handleImageSelect: (image: ImageItem | null) => void;
|
|
57
|
+
handleCancel: () => void;
|
|
58
|
+
handleRemoveImage: (image: ImageItem) => Promise<void>;
|
|
59
|
+
handleAfterUpload: (image: any) => Promise<void>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface UseImageZoomReturn {
|
|
63
|
+
zoomLevel: number;
|
|
64
|
+
selectedImageIndex: number;
|
|
65
|
+
setZoomLevel: (level: number) => void;
|
|
66
|
+
setSelectedImageIndex: (index: number) => void;
|
|
67
|
+
handleZoomIn: () => void;
|
|
68
|
+
handleZoomOut: () => void;
|
|
69
|
+
handlePreviousImage: () => string;
|
|
70
|
+
handleNextImage: () => string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface UseClipboardReturn {
|
|
74
|
+
clipboardSupported: boolean;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Component props interfaces
|
|
78
|
+
export interface ImageGalleryGridProps {
|
|
79
|
+
imageList: ImageItem[];
|
|
80
|
+
emptyMessage: string;
|
|
81
|
+
multiple?: boolean;
|
|
82
|
+
updatable?: boolean;
|
|
83
|
+
removable?: boolean;
|
|
84
|
+
allowDownload?: boolean;
|
|
85
|
+
showImageInfo?: boolean;
|
|
86
|
+
loading?: boolean;
|
|
87
|
+
clipboardSupported?: boolean;
|
|
88
|
+
getImageUrl: (params: { imageCode: string; thumbMail?: boolean }) => string;
|
|
89
|
+
noImageUrl: string;
|
|
90
|
+
onImageClick: (imageUrl: string, index: number) => void;
|
|
91
|
+
onImageSelect: (image: ImageItem | null) => void;
|
|
92
|
+
onRemoveImage: (image: ImageItem) => void;
|
|
93
|
+
onDownloadImage: (imageCode: string) => void;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface ImageGalleryModalProps {
|
|
97
|
+
open: boolean;
|
|
98
|
+
imageUrl?: string | null;
|
|
99
|
+
imageList: ImageItem[];
|
|
100
|
+
selectedImageIndex: number;
|
|
101
|
+
zoomLevel: number;
|
|
102
|
+
noImageUrl: string;
|
|
103
|
+
onClose: () => void;
|
|
104
|
+
onZoomIn: () => void;
|
|
105
|
+
onZoomOut: () => void;
|
|
106
|
+
onPreviousImage: () => void;
|
|
107
|
+
onNextImage: () => void;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface ImageGalleryItemProps {
|
|
111
|
+
image: ImageItem;
|
|
112
|
+
index: number;
|
|
113
|
+
getImageUrl: (params: { imageCode: string; thumbMail?: boolean }) => string;
|
|
114
|
+
noImageUrl: string;
|
|
115
|
+
loading?: boolean;
|
|
116
|
+
updatable?: boolean;
|
|
117
|
+
removable?: boolean;
|
|
118
|
+
allowDownload?: boolean;
|
|
119
|
+
showImageInfo?: boolean;
|
|
120
|
+
onImageClick: (imageUrl: string, index: number) => void;
|
|
121
|
+
onImageSelect: (image: ImageItem) => void;
|
|
122
|
+
onRemoveImage: (image: ImageItem) => void;
|
|
123
|
+
onDownloadImage: (imageCode: string) => void;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface ImageGalleryEmptyStateProps {
|
|
127
|
+
emptyMessage: string;
|
|
128
|
+
updatable?: boolean;
|
|
129
|
+
clipboardSupported?: boolean;
|
|
130
|
+
onAddImage?: () => void;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Utility function types
|
|
134
|
+
export type HandleImageError = (e: Event, noImageUrl: string) => void;
|
|
135
|
+
export type DownloadImage = (imageUrl: string, imageCode: string) => void;
|
|
136
|
+
export type ProcessImageCodes = (imageCodes: string | string[]) => ImageItem[];
|
|
137
|
+
export type GetGridColumns = (isMobile: boolean, isTablet: boolean, multiple: boolean) => number;
|
|
138
|
+
export type IsValidImageFile = (file: File) => boolean;
|
|
139
|
+
export type FormatFileSize = (bytes: number) => string;
|
|
140
|
+
export type GenerateId = () => string;
|
|
141
|
+
export type Debounce = (func: Function, wait: number) => Function;
|
|
142
|
+
export type IsTouchDevice = () => boolean;
|
|
143
|
+
export type GetImageDimensions = (url: string) => Promise<{ width: number; height: number }>;
|
|
144
|
+
export type CreateImagePreview = (file: File) => Promise<string>;
|
|
145
|
+
|
|
146
|
+
// Hook functions
|
|
147
|
+
export declare function useImageGallery(options: UseImageGalleryOptions): UseImageGalleryReturn;
|
|
148
|
+
export declare function useImageZoom(
|
|
149
|
+
getImageUrl: (params: { imageCode: string }) => string,
|
|
150
|
+
imageList: ImageItem[]
|
|
151
|
+
): UseImageZoomReturn;
|
|
152
|
+
export declare function useClipboard(
|
|
153
|
+
updatable: boolean,
|
|
154
|
+
selectingImage: boolean,
|
|
155
|
+
onPasteImage?: (file: File) => void
|
|
156
|
+
): UseClipboardReturn;
|
|
157
|
+
export declare function useKeyboardNavigation(
|
|
158
|
+
openImage: string | null,
|
|
159
|
+
onPreviousImage: () => void,
|
|
160
|
+
onNextImage: () => void,
|
|
161
|
+
onCloseImage: () => void,
|
|
162
|
+
onZoomIn: () => void,
|
|
163
|
+
onZoomOut: () => void
|
|
164
|
+
): void;
|
|
165
|
+
|
|
166
|
+
// Main component
|
|
167
|
+
declare const GoalImageGallery: React.FC<GoalImageGalleryProps>;
|
|
168
|
+
export default GoalImageGallery;
|
|
169
|
+
|
|
170
|
+
// Sub-components
|
|
171
|
+
export declare const ImageGalleryGrid: React.FC<ImageGalleryGridProps>;
|
|
172
|
+
export declare const ImageGalleryModal: React.FC<ImageGalleryModalProps>;
|
|
173
|
+
export declare const ImageGalleryItem: React.FC<ImageGalleryItemProps>;
|
|
174
|
+
export declare const ImageGalleryEmptyState: React.FC<ImageGalleryEmptyStateProps>;
|
|
175
|
+
|
|
176
|
+
// Utility functions
|
|
177
|
+
export declare const handleImageError: HandleImageError;
|
|
178
|
+
export declare const downloadImage: DownloadImage;
|
|
179
|
+
export declare const processImageCodes: ProcessImageCodes;
|
|
180
|
+
export declare const getGridColumns: GetGridColumns;
|
|
181
|
+
export declare const isValidImageFile: IsValidImageFile;
|
|
182
|
+
export declare const formatFileSize: FormatFileSize;
|
|
183
|
+
export declare const generateId: GenerateId;
|
|
184
|
+
export declare const debounce: Debounce;
|
|
185
|
+
export declare const isTouchDevice: IsTouchDevice;
|
|
186
|
+
export declare const getImageDimensions: GetImageDimensions;
|
|
187
|
+
export declare const createImagePreview: CreateImagePreview;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// GoalImageGallery - Enhanced Image Gallery Component
|
|
2
|
+
//
|
|
3
|
+
// This is the main entry point for the GoalImageGallery component
|
|
4
|
+
// and its related utilities and documentation.
|
|
5
|
+
|
|
6
|
+
// Main component exports
|
|
7
|
+
export { default as GoalImageGallery } from './GoalImageGallery';
|
|
8
|
+
export { default } from './GoalImageGallery';
|
|
9
|
+
|
|
10
|
+
// Subcomponent exports
|
|
11
|
+
export { default as ImageGalleryGrid } from './components/ImageGalleryGrid';
|
|
12
|
+
export { default as ImageGalleryModal } from './components/ImageGalleryModal';
|
|
13
|
+
export { default as ImageGalleryItem } from './components/ImageGalleryItem';
|
|
14
|
+
export { default as ImageGalleryEmptyState } from './components/ImageGalleryEmptyState';
|
|
15
|
+
|
|
16
|
+
// Hook exports
|
|
17
|
+
export { useImageGallery, useImageZoom, useClipboard, useKeyboardNavigation } from './hooks/use-image-gallery';
|
|
18
|
+
|
|
19
|
+
// Style exports
|
|
20
|
+
export { StyledImageContainer, ImageOverlay, StyledModalContainer, StyledModalHeader, StyledModalBody, StyledModalFooter, StyledEmptyStateContainer, StyledImageGridContainer } from './styles/styled-components';
|
|
21
|
+
|
|
22
|
+
// Utility exports
|
|
23
|
+
export { handleImageError, downloadImage, processImageCodes, getGridColumns, isValidImageFile, formatFileSize, generateId, debounce, isTouchDevice, getImageDimensions, createImagePreview } from './utils/image-utils';
|
|
24
|
+
|
|
25
|
+
// Examples and documentation
|
|
26
|
+
export { default as GoalImageGalleryExamples } from './examples';
|
|
27
|
+
|
|
28
|
+
// Component features and capabilities
|
|
29
|
+
export const FEATURES = {
|
|
30
|
+
UPLOAD_METHODS: {
|
|
31
|
+
FILE_UPLOAD: 'file',
|
|
32
|
+
CLIPBOARD_PASTE: 'clipboard',
|
|
33
|
+
URL_LOADING: 'url'
|
|
34
|
+
},
|
|
35
|
+
KEYBOARD_SHORTCUTS: {
|
|
36
|
+
NAVIGATE_LEFT: 'ArrowLeft',
|
|
37
|
+
NAVIGATE_RIGHT: 'ArrowRight',
|
|
38
|
+
ZOOM_IN: ['+', '='],
|
|
39
|
+
ZOOM_OUT: '-',
|
|
40
|
+
CLOSE: 'Escape',
|
|
41
|
+
PASTE: 'Ctrl+V'
|
|
42
|
+
},
|
|
43
|
+
SUPPORTED_FORMATS: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
|
|
44
|
+
BROWSER_SUPPORT: {
|
|
45
|
+
CHROME: 'full',
|
|
46
|
+
FIREFOX: 'full',
|
|
47
|
+
SAFARI: 'full',
|
|
48
|
+
EDGE: 'full',
|
|
49
|
+
IE11: 'limited'
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Utility functions
|
|
54
|
+
export const utils = {
|
|
55
|
+
/**
|
|
56
|
+
* Convert image URLs to image codes
|
|
57
|
+
* @param {string} url - Image URL
|
|
58
|
+
* @returns {string} - Image code
|
|
59
|
+
*/
|
|
60
|
+
getImageCodeFromUrl: url => {
|
|
61
|
+
return url.split('/').pop().split('.')[0];
|
|
62
|
+
},
|
|
63
|
+
/**
|
|
64
|
+
* Validate image file
|
|
65
|
+
* @param {File} file - Image file
|
|
66
|
+
* @param {Object} options - Validation options
|
|
67
|
+
* @returns {Object} - Validation result
|
|
68
|
+
*/
|
|
69
|
+
validateImageFile: function (file) {
|
|
70
|
+
let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
71
|
+
const {
|
|
72
|
+
maxSize = 5 * 1024 * 1024,
|
|
73
|
+
// 5MB
|
|
74
|
+
allowedTypes = FEATURES.SUPPORTED_FORMATS
|
|
75
|
+
} = options;
|
|
76
|
+
const errors = [];
|
|
77
|
+
if (file.size > maxSize) {
|
|
78
|
+
errors.push(`File size exceeds ${maxSize / 1024 / 1024}MB limit`);
|
|
79
|
+
}
|
|
80
|
+
if (!allowedTypes.includes(file.type)) {
|
|
81
|
+
errors.push(`File type ${file.type} is not supported`);
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
isValid: errors.length === 0,
|
|
85
|
+
errors
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
/**
|
|
89
|
+
* Create image handler API wrapper
|
|
90
|
+
* @param {Function} uploadFunction - Upload function
|
|
91
|
+
* @returns {Function} - Wrapped handler
|
|
92
|
+
*/
|
|
93
|
+
createImageHandler: uploadFunction => {
|
|
94
|
+
return async imageData => {
|
|
95
|
+
try {
|
|
96
|
+
const result = await uploadFunction(imageData);
|
|
97
|
+
return {
|
|
98
|
+
success: true,
|
|
99
|
+
data: result,
|
|
100
|
+
message: 'Upload successful'
|
|
101
|
+
};
|
|
102
|
+
} catch (error) {
|
|
103
|
+
return {
|
|
104
|
+
success: false,
|
|
105
|
+
message: error.message
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
},
|
|
110
|
+
/**
|
|
111
|
+
* Convert legacy photo format to image codes
|
|
112
|
+
* @param {Array} photos - Legacy photo array
|
|
113
|
+
* @returns {Array} - Image codes
|
|
114
|
+
*/
|
|
115
|
+
convertPhotosToImageCodes: photos => {
|
|
116
|
+
return photos.map(photo => photo.image_code || photo.id || photo.src);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// Configuration constants
|
|
121
|
+
export const CONFIG = {
|
|
122
|
+
DEFAULT_MAX_FILES: 10,
|
|
123
|
+
DEFAULT_MAX_FILE_SIZE: 5 * 1024 * 1024,
|
|
124
|
+
// 5MB
|
|
125
|
+
DEFAULT_UPLOAD_FOLDER: 'gallery',
|
|
126
|
+
DEFAULT_ZOOM_LEVELS: {
|
|
127
|
+
MIN: 0.5,
|
|
128
|
+
MAX: 3.0,
|
|
129
|
+
STEP: 0.5
|
|
130
|
+
},
|
|
131
|
+
DEFAULT_BREAKPOINTS: {
|
|
132
|
+
MOBILE: 'md',
|
|
133
|
+
TABLET: 'lg',
|
|
134
|
+
DESKTOP: 'xl'
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// Export version info
|
|
139
|
+
export const VERSION = '2.0.0';
|
|
140
|
+
export const CHANGELOG = [{
|
|
141
|
+
version: '2.0.0',
|
|
142
|
+
date: '2024-01-01',
|
|
143
|
+
changes: ['Added multi-method upload support', 'Enhanced UI/UX with Material Design 3', 'Improved accessibility features', 'Added keyboard navigation', 'Mobile optimization', 'Comprehensive documentation']
|
|
144
|
+
}, {
|
|
145
|
+
version: '1.0.0',
|
|
146
|
+
date: '2023-12-01',
|
|
147
|
+
changes: ['Initial release', 'Basic image gallery functionality', 'File upload support', 'Image viewer modal']
|
|
148
|
+
}];
|