@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.
@@ -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
+ }];