@visns-studio/visns-components 5.13.7 → 5.13.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"react-dom": "^17.0.0 || ^18.0.0"
|
|
89
89
|
},
|
|
90
90
|
"name": "@visns-studio/visns-components",
|
|
91
|
-
"version": "5.13.
|
|
91
|
+
"version": "5.13.8",
|
|
92
92
|
"description": "Various packages to assist in the development of our Custom Applications.",
|
|
93
93
|
"main": "src/index.js",
|
|
94
94
|
"files": [
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
SortableContainer,
|
|
4
4
|
SortableElement,
|
|
5
5
|
sortableHandle,
|
|
6
6
|
} from 'react-sortable-hoc';
|
|
7
7
|
import { GripVertical as ChevronVertical, Edit as Pencil, Trash2 as TrashCan } from 'lucide-react';
|
|
8
|
+
import ImageModal from './ImageModal';
|
|
8
9
|
import styles from './styles/Gallery.module.css';
|
|
9
10
|
|
|
10
11
|
// Drag handle component
|
|
@@ -24,7 +25,7 @@ const truncateText = (text, maxLength) => {
|
|
|
24
25
|
|
|
25
26
|
// Sortable gallery item
|
|
26
27
|
const SortableGalleryItem = SortableElement(
|
|
27
|
-
({ value, handleEdit, handleDelete, showDescription = true }) => {
|
|
28
|
+
({ value, handleEdit, handleDelete, showDescription = true, onImageClick }) => {
|
|
28
29
|
const imageUrl = value.image_url || value.file_url;
|
|
29
30
|
const title =
|
|
30
31
|
value.file_title || value.label || value.file_name || 'Untitled';
|
|
@@ -34,7 +35,11 @@ const SortableGalleryItem = SortableElement(
|
|
|
34
35
|
<div className={styles.galleryItem}>
|
|
35
36
|
<DragHandle />
|
|
36
37
|
|
|
37
|
-
<div
|
|
38
|
+
<div
|
|
39
|
+
className={styles.galleryImage}
|
|
40
|
+
onClick={() => onImageClick && onImageClick(value)}
|
|
41
|
+
style={{ cursor: onImageClick ? 'pointer' : 'default' }}
|
|
42
|
+
>
|
|
38
43
|
{imageUrl ? (
|
|
39
44
|
<img src={imageUrl} alt={title} />
|
|
40
45
|
) : (
|
|
@@ -95,6 +100,30 @@ const SortableGalleryItem = SortableElement(
|
|
|
95
100
|
|
|
96
101
|
// Sortable gallery container
|
|
97
102
|
const Gallery = SortableContainer(({ items, handleEdit, handleDelete, showDescription = true }) => {
|
|
103
|
+
const [modalOpen, setModalOpen] = React.useState(false);
|
|
104
|
+
const [currentImageIndex, setCurrentImageIndex] = React.useState(0);
|
|
105
|
+
|
|
106
|
+
const handleImageClick = (clickedImage) => {
|
|
107
|
+
const imageIndex = items.findIndex(item => item.id === clickedImage.id);
|
|
108
|
+
setCurrentImageIndex(imageIndex);
|
|
109
|
+
setModalOpen(true);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const handleNextImage = () => {
|
|
113
|
+
setCurrentImageIndex((prevIndex) =>
|
|
114
|
+
prevIndex < items.length - 1 ? prevIndex + 1 : 0
|
|
115
|
+
);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const handlePrevImage = () => {
|
|
119
|
+
setCurrentImageIndex((prevIndex) =>
|
|
120
|
+
prevIndex > 0 ? prevIndex - 1 : items.length - 1
|
|
121
|
+
);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const handleCloseModal = () => {
|
|
125
|
+
setModalOpen(false);
|
|
126
|
+
};
|
|
98
127
|
if (!items || items.length === 0) {
|
|
99
128
|
return (
|
|
100
129
|
<div className={styles.galleryEmpty}>
|
|
@@ -130,18 +159,29 @@ const Gallery = SortableContainer(({ items, handleEdit, handleDelete, showDescri
|
|
|
130
159
|
}
|
|
131
160
|
|
|
132
161
|
return (
|
|
133
|
-
|
|
134
|
-
{
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
162
|
+
<>
|
|
163
|
+
<div className={styles.galleryGrid}>
|
|
164
|
+
{items.map((value, index) => (
|
|
165
|
+
<SortableGalleryItem
|
|
166
|
+
key={`item-${index}`}
|
|
167
|
+
index={index}
|
|
168
|
+
value={value}
|
|
169
|
+
handleEdit={handleEdit}
|
|
170
|
+
handleDelete={handleDelete}
|
|
171
|
+
showDescription={showDescription}
|
|
172
|
+
onImageClick={handleImageClick}
|
|
173
|
+
/>
|
|
174
|
+
))}
|
|
175
|
+
</div>
|
|
176
|
+
<ImageModal
|
|
177
|
+
isOpen={modalOpen}
|
|
178
|
+
onClose={handleCloseModal}
|
|
179
|
+
images={items}
|
|
180
|
+
currentIndex={currentImageIndex}
|
|
181
|
+
onNext={handleNextImage}
|
|
182
|
+
onPrevious={handlePrevImage}
|
|
183
|
+
/>
|
|
184
|
+
</>
|
|
145
185
|
);
|
|
146
186
|
});
|
|
147
187
|
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import React, { useEffect, useCallback } from 'react';
|
|
2
|
+
import { X as CloseIcon, ChevronLeft, ChevronRight } from 'lucide-react';
|
|
3
|
+
import styles from './styles/ImageModal.module.css';
|
|
4
|
+
|
|
5
|
+
const ImageModal = ({
|
|
6
|
+
isOpen,
|
|
7
|
+
onClose,
|
|
8
|
+
images,
|
|
9
|
+
currentIndex,
|
|
10
|
+
onNext,
|
|
11
|
+
onPrevious,
|
|
12
|
+
showNavigation = true
|
|
13
|
+
}) => {
|
|
14
|
+
const handleKeyDown = useCallback((e) => {
|
|
15
|
+
if (!isOpen) return;
|
|
16
|
+
|
|
17
|
+
switch (e.key) {
|
|
18
|
+
case 'Escape':
|
|
19
|
+
onClose();
|
|
20
|
+
break;
|
|
21
|
+
case 'ArrowLeft':
|
|
22
|
+
e.preventDefault();
|
|
23
|
+
onPrevious();
|
|
24
|
+
break;
|
|
25
|
+
case 'ArrowRight':
|
|
26
|
+
e.preventDefault();
|
|
27
|
+
onNext();
|
|
28
|
+
break;
|
|
29
|
+
default:
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
}, [isOpen, onClose, onNext, onPrevious]);
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (isOpen) {
|
|
36
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
37
|
+
document.body.style.overflow = 'hidden';
|
|
38
|
+
} else {
|
|
39
|
+
document.body.style.overflow = 'unset';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return () => {
|
|
43
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
44
|
+
document.body.style.overflow = 'unset';
|
|
45
|
+
};
|
|
46
|
+
}, [isOpen, handleKeyDown]);
|
|
47
|
+
|
|
48
|
+
if (!isOpen || !images || images.length === 0) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const currentImage = images[currentIndex];
|
|
53
|
+
const imageUrl = currentImage?.image_url || currentImage?.file_url;
|
|
54
|
+
const title = currentImage?.file_title || currentImage?.label || currentImage?.file_name || 'Untitled';
|
|
55
|
+
const description = currentImage?.file_description || '';
|
|
56
|
+
|
|
57
|
+
const handleBackdropClick = (e) => {
|
|
58
|
+
if (e.target === e.currentTarget) {
|
|
59
|
+
onClose();
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div className={styles.modalOverlay} onClick={handleBackdropClick}>
|
|
65
|
+
<div className={styles.modalContent}>
|
|
66
|
+
<button
|
|
67
|
+
className={styles.closeButton}
|
|
68
|
+
onClick={onClose}
|
|
69
|
+
aria-label="Close modal"
|
|
70
|
+
>
|
|
71
|
+
<CloseIcon size={24} />
|
|
72
|
+
</button>
|
|
73
|
+
|
|
74
|
+
{showNavigation && images.length > 1 && (
|
|
75
|
+
<>
|
|
76
|
+
<button
|
|
77
|
+
className={`${styles.navButton} ${styles.prevButton}`}
|
|
78
|
+
onClick={onPrevious}
|
|
79
|
+
aria-label="Previous image"
|
|
80
|
+
disabled={currentIndex === 0}
|
|
81
|
+
>
|
|
82
|
+
<ChevronLeft size={32} />
|
|
83
|
+
</button>
|
|
84
|
+
<button
|
|
85
|
+
className={`${styles.navButton} ${styles.nextButton}`}
|
|
86
|
+
onClick={onNext}
|
|
87
|
+
aria-label="Next image"
|
|
88
|
+
disabled={currentIndex === images.length - 1}
|
|
89
|
+
>
|
|
90
|
+
<ChevronRight size={32} />
|
|
91
|
+
</button>
|
|
92
|
+
</>
|
|
93
|
+
)}
|
|
94
|
+
|
|
95
|
+
<div className={styles.imageContainer}>
|
|
96
|
+
<img
|
|
97
|
+
src={imageUrl}
|
|
98
|
+
alt={title}
|
|
99
|
+
className={styles.modalImage}
|
|
100
|
+
loading="lazy"
|
|
101
|
+
/>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<div className={styles.imageInfo}>
|
|
105
|
+
<h3 className={styles.imageTitle}>{title}</h3>
|
|
106
|
+
{description && (
|
|
107
|
+
<p className={styles.imageDescription}>{description}</p>
|
|
108
|
+
)}
|
|
109
|
+
{images.length > 1 && (
|
|
110
|
+
<div className={styles.imageCounter}>
|
|
111
|
+
{currentIndex + 1} of {images.length}
|
|
112
|
+
</div>
|
|
113
|
+
)}
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export default ImageModal;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
.modalOverlay {
|
|
2
|
+
position: fixed;
|
|
3
|
+
top: 0;
|
|
4
|
+
left: 0;
|
|
5
|
+
right: 0;
|
|
6
|
+
bottom: 0;
|
|
7
|
+
background-color: rgba(0, 0, 0, 0.9);
|
|
8
|
+
display: flex;
|
|
9
|
+
align-items: center;
|
|
10
|
+
justify-content: center;
|
|
11
|
+
z-index: 1000;
|
|
12
|
+
padding: 20px;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.modalContent {
|
|
16
|
+
position: relative;
|
|
17
|
+
max-width: 95vw;
|
|
18
|
+
max-height: 95vh;
|
|
19
|
+
width: 100%;
|
|
20
|
+
height: 100%;
|
|
21
|
+
display: flex;
|
|
22
|
+
flex-direction: column;
|
|
23
|
+
align-items: center;
|
|
24
|
+
justify-content: center;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.closeButton {
|
|
28
|
+
position: absolute;
|
|
29
|
+
top: 20px;
|
|
30
|
+
right: 20px;
|
|
31
|
+
background: rgba(255, 255, 255, 0.9);
|
|
32
|
+
border: none;
|
|
33
|
+
border-radius: 50%;
|
|
34
|
+
width: 44px;
|
|
35
|
+
height: 44px;
|
|
36
|
+
display: flex;
|
|
37
|
+
align-items: center;
|
|
38
|
+
justify-content: center;
|
|
39
|
+
cursor: pointer;
|
|
40
|
+
z-index: 1001;
|
|
41
|
+
transition: all 0.2s ease;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.closeButton:hover {
|
|
45
|
+
background: rgba(255, 255, 255, 1);
|
|
46
|
+
transform: scale(1.1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.navButton {
|
|
50
|
+
position: absolute;
|
|
51
|
+
top: 50%;
|
|
52
|
+
transform: translateY(-50%);
|
|
53
|
+
background: rgba(255, 255, 255, 0.9);
|
|
54
|
+
border: none;
|
|
55
|
+
border-radius: 50%;
|
|
56
|
+
width: 56px;
|
|
57
|
+
height: 56px;
|
|
58
|
+
display: flex;
|
|
59
|
+
align-items: center;
|
|
60
|
+
justify-content: center;
|
|
61
|
+
cursor: pointer;
|
|
62
|
+
z-index: 1001;
|
|
63
|
+
transition: all 0.2s ease;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.navButton:hover {
|
|
67
|
+
background: rgba(255, 255, 255, 1);
|
|
68
|
+
transform: translateY(-50%) scale(1.1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.navButton:disabled {
|
|
72
|
+
opacity: 0.5;
|
|
73
|
+
cursor: not-allowed;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.navButton:disabled:hover {
|
|
77
|
+
background: rgba(255, 255, 255, 0.9);
|
|
78
|
+
transform: translateY(-50%) scale(1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.prevButton {
|
|
82
|
+
left: 20px;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.nextButton {
|
|
86
|
+
right: 20px;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.imageContainer {
|
|
90
|
+
flex: 1;
|
|
91
|
+
display: flex;
|
|
92
|
+
align-items: center;
|
|
93
|
+
justify-content: center;
|
|
94
|
+
max-width: 100%;
|
|
95
|
+
max-height: 100%;
|
|
96
|
+
overflow: hidden;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.modalImage {
|
|
100
|
+
max-width: 100%;
|
|
101
|
+
max-height: 100%;
|
|
102
|
+
object-fit: contain;
|
|
103
|
+
border-radius: 8px;
|
|
104
|
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.imageInfo {
|
|
108
|
+
position: absolute;
|
|
109
|
+
bottom: 20px;
|
|
110
|
+
left: 20px;
|
|
111
|
+
right: 20px;
|
|
112
|
+
background: rgba(0, 0, 0, 0.7);
|
|
113
|
+
color: white;
|
|
114
|
+
padding: 16px;
|
|
115
|
+
border-radius: 8px;
|
|
116
|
+
text-align: center;
|
|
117
|
+
max-width: 500px;
|
|
118
|
+
margin: 0 auto;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.imageTitle {
|
|
122
|
+
margin: 0 0 8px 0;
|
|
123
|
+
font-size: 18px;
|
|
124
|
+
font-weight: 600;
|
|
125
|
+
color: white;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.imageDescription {
|
|
129
|
+
margin: 0 0 8px 0;
|
|
130
|
+
font-size: 14px;
|
|
131
|
+
color: rgba(255, 255, 255, 0.9);
|
|
132
|
+
line-height: 1.4;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.imageCounter {
|
|
136
|
+
font-size: 12px;
|
|
137
|
+
color: rgba(255, 255, 255, 0.7);
|
|
138
|
+
margin: 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* Mobile responsive styles */
|
|
142
|
+
@media (max-width: 768px) {
|
|
143
|
+
.modalOverlay {
|
|
144
|
+
padding: 10px;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.closeButton {
|
|
148
|
+
top: 10px;
|
|
149
|
+
right: 10px;
|
|
150
|
+
width: 40px;
|
|
151
|
+
height: 40px;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.navButton {
|
|
155
|
+
width: 48px;
|
|
156
|
+
height: 48px;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.prevButton {
|
|
160
|
+
left: 10px;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.nextButton {
|
|
164
|
+
right: 10px;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.imageInfo {
|
|
168
|
+
bottom: 10px;
|
|
169
|
+
left: 10px;
|
|
170
|
+
right: 10px;
|
|
171
|
+
padding: 12px;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.imageTitle {
|
|
175
|
+
font-size: 16px;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.imageDescription {
|
|
179
|
+
font-size: 13px;
|
|
180
|
+
}
|
|
181
|
+
}
|
package/src/index.js
CHANGED
|
@@ -34,6 +34,7 @@ import DataGridSearch from './components/controls/DataGridSearch';
|
|
|
34
34
|
import AutoRefreshControls from './components/controls/AutoRefreshControls';
|
|
35
35
|
import AudioPlayer from './components/controls/AudioPlayer';
|
|
36
36
|
import GalleryModal from './components/modals/GalleryModal';
|
|
37
|
+
import ImageModal from './components/ImageModal';
|
|
37
38
|
import DatePickerPortal from './components/utils/DatePickerPortal';
|
|
38
39
|
export * from './components/columns/ColumnRenderers.jsx';
|
|
39
40
|
|
|
@@ -110,6 +111,7 @@ export {
|
|
|
110
111
|
Field,
|
|
111
112
|
Form,
|
|
112
113
|
GalleryModal,
|
|
114
|
+
ImageModal,
|
|
113
115
|
GenericAuth,
|
|
114
116
|
GenericClientPortal,
|
|
115
117
|
GenericDashboard,
|