@visns-studio/visns-components 5.13.7 → 5.13.9

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.7",
91
+ "version": "5.13.9",
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 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;
@@ -370,7 +370,7 @@ const GenericEditableTable = ({
370
370
  if (coloursValue) {
371
371
  return `${coloursValue}40`; // Add 40% opacity
372
372
  }
373
-
373
+
374
374
  // Check for status color if no manual color is set
375
375
  const statusColor = getStatusColor(entry);
376
376
  return statusColor ? `${statusColor}40` : undefined; // Add 40% opacity
@@ -396,11 +396,10 @@ const GenericEditableTable = ({
396
396
  return statusOption?.colour || undefined;
397
397
  }
398
398
  }
399
-
399
+
400
400
  return undefined;
401
401
  };
402
402
 
403
-
404
403
  // Calculate total project cost (sum of all entries excluding margin row)
405
404
  const calculateTotalProjectCost = (dataEntries) => {
406
405
  const totalColIndex = columns.findIndex((c) => c.type === 'total');
@@ -425,8 +424,8 @@ const GenericEditableTable = ({
425
424
  if (!whereConditions || !Array.isArray(whereConditions)) {
426
425
  return true;
427
426
  }
428
-
429
- return whereConditions.every(condition => {
427
+
428
+ return whereConditions.every((condition) => {
430
429
  const { id, value } = condition;
431
430
  return row[id] === value;
432
431
  });
@@ -472,9 +471,11 @@ const GenericEditableTable = ({
472
471
  if (!data || !Array.isArray(data)) {
473
472
  return 0;
474
473
  }
475
-
476
- const filteredData = data.filter(row => matchesWhereConditions(row, whereConditions));
477
-
474
+
475
+ const filteredData = data.filter((row) =>
476
+ matchesWhereConditions(row, whereConditions)
477
+ );
478
+
478
479
  return filteredData.reduce((sum, entry) => {
479
480
  return sum + calculateTotal(entry, keys);
480
481
  }, 0);
@@ -1294,7 +1295,7 @@ const GenericEditableTable = ({
1294
1295
  textAlign: 'left',
1295
1296
  width: '20%',
1296
1297
  }}
1297
- colSpan={3}
1298
+ colSpan={2}
1298
1299
  >
1299
1300
  Margin %
1300
1301
  </th>
@@ -1307,6 +1308,7 @@ const GenericEditableTable = ({
1307
1308
  textAlign: 'right',
1308
1309
  width: '20%',
1309
1310
  }}
1311
+ colSpan={2}
1310
1312
  >
1311
1313
  Total
1312
1314
  </th>
@@ -1325,7 +1327,7 @@ const GenericEditableTable = ({
1325
1327
  {/* Margin % input */}
1326
1328
  <td
1327
1329
  style={{ textAlign: 'center', padding: '8px' }}
1328
- colSpan={3}
1330
+ colSpan={2}
1329
1331
  >
1330
1332
  <input
1331
1333
  type="number"
@@ -1351,6 +1353,7 @@ const GenericEditableTable = ({
1351
1353
  fontWeight: 'bold',
1352
1354
  padding: '8px',
1353
1355
  }}
1356
+ colSpan={2}
1354
1357
  >
1355
1358
  {totalWithMargin.toLocaleString('en-AU', {
1356
1359
  style: 'currency',
@@ -1661,10 +1664,16 @@ const GenericEditableTable = ({
1661
1664
  );
1662
1665
  }
1663
1666
  if (index === totalColIndex) {
1664
- const totalColumn = columns[totalColIndex];
1667
+ const totalColumn =
1668
+ columns[totalColIndex];
1665
1669
  const groupTotal =
1666
1670
  group.entries
1667
- .filter(entry => matchesWhereConditions(entry, totalColumn.where))
1671
+ .filter((entry) =>
1672
+ matchesWhereConditions(
1673
+ entry,
1674
+ totalColumn.where
1675
+ )
1676
+ )
1668
1677
  .reduce(
1669
1678
  (sum, entry) =>
1670
1679
  sum +
@@ -1728,11 +1737,9 @@ const GenericEditableTable = ({
1728
1737
  <td
1729
1738
  key={column.id}
1730
1739
  style={{
1731
- width:
1732
- column.width || 'auto',
1740
+ width: column.width || 'auto',
1733
1741
  textAlign:
1734
- column.type ===
1735
- 'currency'
1742
+ column.type === 'currency'
1736
1743
  ? 'right'
1737
1744
  : 'left',
1738
1745
  }}
@@ -1817,21 +1824,25 @@ const GenericEditableTable = ({
1817
1824
  );
1818
1825
  }
1819
1826
  if (index === totalColIndex) {
1820
- const totalColumn = columns[totalColIndex];
1827
+ const totalColumn =
1828
+ columns[totalColIndex];
1821
1829
  const overallTotal =
1822
- sortBySortOrder(
1823
- localData
1824
- )
1825
- .filter(entry => matchesWhereConditions(entry, totalColumn.where))
1826
- .reduce(
1827
- (sum, entry) =>
1828
- sum +
1829
- calculateTotal(
1830
+ sortBySortOrder(localData)
1831
+ .filter((entry) =>
1832
+ matchesWhereConditions(
1830
1833
  entry,
1831
- totalColumn.keys
1832
- ),
1833
- 0
1834
- );
1834
+ totalColumn.where
1835
+ )
1836
+ )
1837
+ .reduce(
1838
+ (sum, entry) =>
1839
+ sum +
1840
+ calculateTotal(
1841
+ entry,
1842
+ totalColumn.keys
1843
+ ),
1844
+ 0
1845
+ );
1835
1846
  return (
1836
1847
  <td key={col.id}>
1837
1848
  {overallTotal.toLocaleString(
@@ -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,