@visns-studio/visns-components 5.13.6 → 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
@@ -24,19 +24,20 @@
24
24
  "dayjs": "^1.11.13",
25
25
  "fabric": "^6.7.0",
26
26
  "file-saver": "^2.0.5",
27
- "framer-motion": "^12.23.0",
27
+ "framer-motion": "^12.23.1",
28
28
  "html-react-parser": "^5.2.5",
29
29
  "lodash": "^4.17.21",
30
30
  "lodash.debounce": "^4.0.8",
31
31
  "lucide-react": "^0.525.0",
32
32
  "moment": "^2.30.1",
33
- "motion": "^12.23.0",
33
+ "motion": "^12.23.1",
34
34
  "numeral": "^2.0.6",
35
35
  "pluralize": "^8.0.0",
36
36
  "qrcode.react": "^4.2.0",
37
37
  "quill-image-uploader": "^1.3.0",
38
38
  "react-big-calendar": "^1.19.4",
39
39
  "react-copy-to-clipboard": "^5.1.0",
40
+ "react-currency-input-field": "^3.10.0",
40
41
  "react-datepicker": "^8.4.0",
41
42
  "react-dropzone": "^14.3.8",
42
43
  "react-grid-gallery": "^1.0.1",
@@ -87,7 +88,7 @@
87
88
  "react-dom": "^17.0.0 || ^18.0.0"
88
89
  },
89
90
  "name": "@visns-studio/visns-components",
90
- "version": "5.13.6",
91
+ "version": "5.13.8",
91
92
  "description": "Various packages to assist in the development of our Custom Applications.",
92
93
  "main": "src/index.js",
93
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 className={styles.galleryImage}>
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
- <div className={styles.galleryGrid}>
134
- {items.map((value, index) => (
135
- <SortableGalleryItem
136
- key={`item-${index}`}
137
- index={index}
138
- value={value}
139
- handleEdit={handleEdit}
140
- handleDelete={handleDelete}
141
- showDescription={showDescription}
142
- />
143
- ))}
144
- </div>
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;
@@ -5,6 +5,7 @@ import { toast } from 'react-toastify';
5
5
  import { Trash2, ArrowUp, ArrowDown, Copy, Droplets } from 'lucide-react';
6
6
  import { Compact } from '@uiw/react-color';
7
7
  import { confirmDialog } from '../utils/ConfirmDialog';
8
+ import CurrencyInput from 'react-currency-input-field';
8
9
 
9
10
  import CustomFetch from '../Fetch';
10
11
  import MultiSelect from '../MultiSelect';
@@ -920,6 +921,27 @@ const GenericEditableTable = ({
920
921
  // For other colour columns, picker is rendered in the action column
921
922
  return null;
922
923
  case 'currency':
924
+ return (
925
+ <CurrencyInput
926
+ id={`currency-${column.id}-${keyCounter}`}
927
+ name={column.id}
928
+ placeholder="$0.00"
929
+ defaultValue={entry[column.id] || ''}
930
+ decimalsLimit={2}
931
+ prefix="$"
932
+ decimalSeparator="."
933
+ groupSeparator=","
934
+ onValueChange={(value) =>
935
+ handleFieldChange(
936
+ value || '',
937
+ column.id,
938
+ keyCounter
939
+ )
940
+ }
941
+ style={{ textAlign: 'right', width: '100%' }}
942
+ readOnly={column.readOnly || false}
943
+ />
944
+ );
923
945
  case 'number':
924
946
  return (
925
947
  <input
@@ -1076,6 +1098,26 @@ const GenericEditableTable = ({
1076
1098
  // For other colour columns, picker is rendered in the action column
1077
1099
  return null;
1078
1100
  case 'currency':
1101
+ return (
1102
+ <CurrencyInput
1103
+ id={`new-currency-${column.id}-${groupId || 'default'}`}
1104
+ name={column.id}
1105
+ placeholder="$0.00"
1106
+ defaultValue={value || ''}
1107
+ decimalsLimit={2}
1108
+ prefix="$"
1109
+ decimalSeparator="."
1110
+ groupSeparator=","
1111
+ onValueChange={(value) =>
1112
+ handleNewFieldChange(
1113
+ value || '',
1114
+ column.id,
1115
+ groupId
1116
+ )
1117
+ }
1118
+ style={{ textAlign: 'right', width: '100%' }}
1119
+ />
1120
+ );
1079
1121
  case 'number':
1080
1122
  return (
1081
1123
  <input
@@ -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,