@visns-studio/visns-components 5.10.1 → 5.10.3

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,405 @@
1
+ import { useState } from 'react';
2
+ import Popup from 'reactjs-popup';
3
+ import { toast } from 'react-toastify';
4
+ import imageCompression from 'browser-image-compression';
5
+ import Vapor from 'laravel-vapor';
6
+ import Lightbox from 'yet-another-react-lightbox';
7
+ import { CircleX, Image as ImageIcon, CloudUpload } from 'akar-icons';
8
+ import styles from '../styles/DataGrid.module.scss';
9
+ import CustomFetch from '../Fetch';
10
+
11
+ const GalleryModal = ({
12
+ modalGalleryShow,
13
+ modalGalleryClose,
14
+ galleryData,
15
+ setGalleryData,
16
+ currentItem,
17
+ form,
18
+ lightboxOpen,
19
+ setLightboxOpen,
20
+ lightboxIndex,
21
+ setLightboxIndex,
22
+ }) => {
23
+ const [uploadProgress, setUploadProgress] = useState(0);
24
+ const [isUploading, setIsUploading] = useState(false);
25
+
26
+ const handleFileUpload = async (files) => {
27
+ if (!currentItem || !currentItem.settings.upload) return;
28
+
29
+ setIsUploading(true);
30
+ const uploadPromises = [];
31
+
32
+ for (let i = 0; i < files.length; i++) {
33
+ const file = files[i];
34
+
35
+ try {
36
+ // Optimize image if needed
37
+ let optimizedFile = file;
38
+ if (file.type.startsWith('image/')) {
39
+ const options = {
40
+ maxSizeMB: 1,
41
+ maxWidthOrHeight: 1920,
42
+ useWebWorker: true,
43
+ };
44
+ try {
45
+ optimizedFile = await imageCompression(
46
+ file,
47
+ options
48
+ );
49
+ } catch (err) {
50
+ console.error('Image compression failed:', err);
51
+ // Continue with original file if compression fails
52
+ }
53
+ }
54
+
55
+ // Upload to Vapor
56
+ const uploadPromise = new Promise((resolve, reject) => {
57
+ Vapor.store(optimizedFile, {
58
+ progress: (progress) => {
59
+ setUploadProgress(progress * 100);
60
+ },
61
+ })
62
+ .then((response) => {
63
+ // Construct the URL with the key parameter
64
+ const uploadSettings =
65
+ currentItem.settings.upload;
66
+ const itemData = currentItem.data;
67
+ const keyValue = itemData[uploadSettings.key];
68
+ const url = keyValue
69
+ ? `${uploadSettings.url}/${keyValue}`
70
+ : uploadSettings.url;
71
+
72
+ // Make the PUT request
73
+ CustomFetch(
74
+ url,
75
+ uploadSettings.method || 'POST',
76
+ {
77
+ uuid: response.uuid,
78
+ key: response.key,
79
+ bucket: response.bucket,
80
+ filename: file.name,
81
+ filesize: optimizedFile.size,
82
+ extension: response.extension,
83
+ file_relationship: uploadSettings.id,
84
+ },
85
+ (result) => {
86
+ if (result.error === '') {
87
+ resolve();
88
+ } else {
89
+ toast.error(String(result.error));
90
+ reject(result.error);
91
+ }
92
+ }
93
+ );
94
+ })
95
+ .catch(reject);
96
+ });
97
+
98
+ uploadPromises.push(uploadPromise);
99
+ } catch (error) {
100
+ console.error('Error uploading file:', error);
101
+ toast.error(
102
+ `Error uploading ${file.name}: ${
103
+ error.message || 'Unknown error'
104
+ }`
105
+ );
106
+ }
107
+ }
108
+
109
+ try {
110
+ await Promise.all(uploadPromises);
111
+ toast.success('Files uploaded successfully');
112
+
113
+ // Refresh the gallery data
114
+ if (currentItem && currentItem.settings && currentItem.data) {
115
+ const s = currentItem.settings;
116
+ const d = currentItem.data;
117
+
118
+ // Function to access nested property using a series of keys
119
+ const getNestedValue = (data, keys) =>
120
+ keys.reduce(
121
+ (currentValue, key) =>
122
+ currentValue && currentValue[key] !== undefined
123
+ ? currentValue[key]
124
+ : undefined,
125
+ data
126
+ );
127
+
128
+ // Fetch updated data
129
+ try {
130
+ const res = await CustomFetch(
131
+ `${form.url}/${d[form.primaryKey]}`,
132
+ 'GET',
133
+ {}
134
+ );
135
+
136
+ if (res.data) {
137
+ // Extract the updated nested value
138
+ const updatedNestedValue = getNestedValue(
139
+ res.data,
140
+ s.relation
141
+ );
142
+
143
+ // Update gallery data
144
+ if (
145
+ updatedNestedValue &&
146
+ Array.isArray(updatedNestedValue)
147
+ ) {
148
+ const updatedGalleryArray =
149
+ updatedNestedValue.map((item) => ({
150
+ src: item[s.key],
151
+ caption: item.file_name,
152
+ }));
153
+
154
+ setGalleryData(updatedGalleryArray);
155
+ }
156
+ }
157
+ } catch (error) {
158
+ console.error('Error refreshing gallery data:', error);
159
+ }
160
+ }
161
+ } catch (error) {
162
+ console.error('Error in upload process:', error);
163
+ } finally {
164
+ setIsUploading(false);
165
+ setUploadProgress(0);
166
+ }
167
+ };
168
+
169
+ return (
170
+ <>
171
+ <Popup
172
+ open={modalGalleryShow}
173
+ onClose={modalGalleryClose}
174
+ closeOnDocumentClick={false}
175
+ contentStyle={{
176
+ width: '90vw',
177
+ maxWidth: '1400px',
178
+ minWidth: '600px',
179
+ padding: 0,
180
+ border: 'none',
181
+ borderRadius: '8px',
182
+ overflow: 'hidden',
183
+ maxHeight: '90vh',
184
+ }}
185
+ overlayStyle={{
186
+ backgroundColor: 'rgba(0, 0, 0, 0.7)',
187
+ }}
188
+ >
189
+ <div className={styles.modalwrap}>
190
+ <div className={styles.modal}>
191
+ <div className={styles.modal__header}>
192
+ <h1>Gallery</h1>
193
+ <button
194
+ className={styles.modal__close}
195
+ onClick={modalGalleryClose}
196
+ >
197
+ <CircleX strokeWidth={1} size={24} />
198
+ </button>
199
+ </div>
200
+ <div
201
+ className={`${styles.modal__content} ${styles['modal__content--gallery']}`}
202
+ >
203
+ {currentItem?.settings?.upload && (
204
+ <div className={styles.uploadSection}>
205
+ <h3>
206
+ <ImageIcon
207
+ strokeWidth={1.5}
208
+ size={18}
209
+ />
210
+ Upload New Images
211
+ </h3>
212
+ <div
213
+ className={styles.dropzoneContainer}
214
+ >
215
+ {isUploading && (
216
+ <div
217
+ className={
218
+ styles.progressBar
219
+ }
220
+ >
221
+ <div
222
+ className={
223
+ styles.progressBarInner
224
+ }
225
+ style={{
226
+ width: `${uploadProgress}%`,
227
+ }}
228
+ ></div>
229
+ </div>
230
+ )}
231
+ <div
232
+ className={styles.dropzone}
233
+ onClick={() =>
234
+ document
235
+ .getElementById(
236
+ 'gallery-file-upload'
237
+ )
238
+ .click()
239
+ }
240
+ onDragOver={(e) => {
241
+ e.preventDefault();
242
+ e.stopPropagation();
243
+ }}
244
+ onDragEnter={(e) => {
245
+ e.preventDefault();
246
+ e.stopPropagation();
247
+ }}
248
+ onDragLeave={(e) => {
249
+ e.preventDefault();
250
+ e.stopPropagation();
251
+ }}
252
+ onDrop={(e) => {
253
+ e.preventDefault();
254
+ e.stopPropagation();
255
+ const files =
256
+ e.dataTransfer.files;
257
+ if (files.length > 0) {
258
+ handleFileUpload(files);
259
+ }
260
+ }}
261
+ >
262
+ <CloudUpload
263
+ strokeWidth={1.5}
264
+ size={24}
265
+ />
266
+ <p>
267
+ Drag and drop files here or
268
+ click to select files
269
+ </p>
270
+ <input
271
+ id="gallery-file-upload"
272
+ type="file"
273
+ multiple
274
+ style={{ display: 'none' }}
275
+ onChange={(e) => {
276
+ if (
277
+ e.target.files
278
+ .length > 0
279
+ ) {
280
+ handleFileUpload(
281
+ e.target.files
282
+ );
283
+ e.target.value =
284
+ null; // Reset input
285
+ }
286
+ }}
287
+ accept="image/*"
288
+ />
289
+ </div>
290
+ </div>
291
+ </div>
292
+ )}
293
+ <div className={styles.formcontainer}>
294
+ {galleryData.length > 0 ? (
295
+ <div
296
+ className={styles.galleryContainer}
297
+ >
298
+ <div
299
+ className={
300
+ styles.galleryInstructions
301
+ }
302
+ >
303
+ <ImageIcon
304
+ strokeWidth={1.5}
305
+ size={16}
306
+ />
307
+ <span>
308
+ Tap any image to enlarge
309
+ </span>
310
+ </div>
311
+ <div
312
+ className={
313
+ styles.customGalleryGrid
314
+ }
315
+ >
316
+ {galleryData.map(
317
+ (img, index) => (
318
+ <div
319
+ key={`gallery-item-${index}`}
320
+ className={
321
+ styles.galleryItem
322
+ }
323
+ onClick={() => {
324
+ setLightboxIndex(
325
+ index
326
+ );
327
+ setLightboxOpen(
328
+ true
329
+ );
330
+ }}
331
+ >
332
+ <img
333
+ src={img.src}
334
+ alt={
335
+ img.caption ||
336
+ `Image ${
337
+ index +
338
+ 1
339
+ }`
340
+ }
341
+ />
342
+ {img.caption && (
343
+ <div
344
+ className={
345
+ styles.imageCaption
346
+ }
347
+ >
348
+ {
349
+ img.caption
350
+ }
351
+ </div>
352
+ )}
353
+ <div
354
+ className={
355
+ styles.galleryImageOverlay
356
+ }
357
+ >
358
+ <span>
359
+ <ImageIcon
360
+ strokeWidth={
361
+ 1.5
362
+ }
363
+ size={
364
+ 16
365
+ }
366
+ style={{
367
+ marginRight:
368
+ '6px',
369
+ }}
370
+ />
371
+ Enlarge
372
+ </span>
373
+ </div>
374
+ </div>
375
+ )
376
+ )}
377
+ </div>
378
+ </div>
379
+ ) : (
380
+ <div className={styles.noDataMessage}>
381
+ <ImageIcon
382
+ strokeWidth={1.5}
383
+ size={20}
384
+ />
385
+ No images available in the gallery
386
+ </div>
387
+ )}
388
+ </div>
389
+ </div>
390
+ </div>
391
+ </div>
392
+ </Popup>
393
+
394
+ {/* Lightbox for full-size image viewing */}
395
+ <Lightbox
396
+ open={lightboxOpen}
397
+ close={() => setLightboxOpen(false)}
398
+ index={lightboxIndex}
399
+ slides={galleryData.map((img) => ({ src: img.src }))}
400
+ />
401
+ </>
402
+ );
403
+ };
404
+
405
+ export default GalleryModal;
@@ -868,3 +868,299 @@
868
868
  user-select: none;
869
869
  }
870
870
  }
871
+
872
+ /* Group action buttons */
873
+ .groupActionContainer {
874
+ display: flex;
875
+ align-items: center;
876
+ flex: 1;
877
+ z-index: 2;
878
+ position: relative;
879
+ }
880
+
881
+ .groupExpandBtn {
882
+ background: var(--primary-color, #3b82f6);
883
+ border: none;
884
+ border-radius: 6px;
885
+ color: white;
886
+ min-width: 28px;
887
+ min-height: 28px;
888
+ width: 28px;
889
+ height: 28px;
890
+ display: flex;
891
+ align-items: center;
892
+ justify-content: center;
893
+ margin-right: 8px;
894
+ cursor: pointer;
895
+ font-size: 12px;
896
+ font-weight: bold;
897
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
898
+ transition: all 0.2s ease;
899
+ position: relative;
900
+
901
+ &:hover {
902
+ transform: scale(1.05);
903
+ background: var(--secondary-color, #2563eb);
904
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
905
+ }
906
+
907
+ &:active {
908
+ transform: scale(0.95);
909
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
910
+ }
911
+
912
+ // Mobile responsive
913
+ @media (max-width: 768px) {
914
+ min-width: 32px;
915
+ min-height: 32px;
916
+ width: 32px;
917
+ height: 32px;
918
+ margin-right: 12px;
919
+ }
920
+
921
+ // Visual indicator for touch devices
922
+ @media (hover: none) and (pointer: coarse) {
923
+ &::after {
924
+ content: '';
925
+ position: absolute;
926
+ top: -2px;
927
+ right: -2px;
928
+ width: 6px;
929
+ height: 6px;
930
+ background: rgba(255, 255, 255, 0.8);
931
+ border-radius: 50%;
932
+ box-shadow: 0 0 4px rgba(255, 255, 255, 0.6);
933
+ animation: touchPulse 2s infinite;
934
+ }
935
+ }
936
+ }
937
+
938
+ .groupActionBtn {
939
+ border: none;
940
+ border-radius: 6px;
941
+ color: white;
942
+ min-width: 28px;
943
+ min-height: 28px;
944
+ width: 28px;
945
+ height: 28px;
946
+ display: flex;
947
+ align-items: center;
948
+ justify-content: center;
949
+ margin-right: 8px;
950
+ cursor: pointer;
951
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
952
+ transition: all 0.2s ease;
953
+ position: relative;
954
+
955
+ &:hover {
956
+ transform: scale(1.05);
957
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
958
+ }
959
+
960
+ &:active {
961
+ transform: scale(0.95);
962
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
963
+ }
964
+
965
+ // Add touch-friendly spacing on mobile
966
+ @media (max-width: 768px) {
967
+ margin-right: 12px;
968
+ min-width: 32px;
969
+ min-height: 32px;
970
+ width: 32px;
971
+ height: 32px;
972
+ }
973
+
974
+ // Visual indicator for touch devices - subtle glow
975
+ @media (hover: none) and (pointer: coarse) {
976
+ &::after {
977
+ content: '';
978
+ position: absolute;
979
+ top: -2px;
980
+ right: -2px;
981
+ width: 6px;
982
+ height: 6px;
983
+ background: rgba(255, 255, 255, 0.8);
984
+ border-radius: 50%;
985
+ box-shadow: 0 0 4px rgba(255, 255, 255, 0.6);
986
+ animation: touchPulse 2s infinite;
987
+ }
988
+ }
989
+
990
+ // Color variants
991
+ &.success {
992
+ background: var(--success-color, #10b981);
993
+ &:hover {
994
+ background: var(--success-hover-color, #059669);
995
+ }
996
+ }
997
+
998
+ &.warning {
999
+ background: var(--warning-color, #f59e0b);
1000
+ &:hover {
1001
+ background: var(--warning-hover-color, #d97706);
1002
+ }
1003
+ }
1004
+
1005
+ &.info {
1006
+ background: var(--info-color, #3b82f6);
1007
+ &:hover {
1008
+ background: var(--info-hover-color, #2563eb);
1009
+ }
1010
+ }
1011
+
1012
+ &.purple {
1013
+ background: var(--purple-color, #8b5cf6);
1014
+ &:hover {
1015
+ background: var(--purple-hover-color, #7c3aed);
1016
+ }
1017
+ }
1018
+
1019
+ &.pink {
1020
+ background: var(--pink-color, #ec4899);
1021
+ &:hover {
1022
+ background: var(--pink-hover-color, #db2777);
1023
+ }
1024
+ }
1025
+ }
1026
+
1027
+ .groupInfo {
1028
+ display: flex;
1029
+ align-items: center;
1030
+ gap: 8px;
1031
+ flex: 1;
1032
+ margin-left: 4px;
1033
+ }
1034
+
1035
+ .groupValue {
1036
+ font-size: 0.85rem;
1037
+ color: var(--secondary-color, #1f2937);
1038
+ font-weight: 600;
1039
+ text-transform: uppercase;
1040
+ letter-spacing: 0.025em;
1041
+ }
1042
+
1043
+ .groupClickIndicator {
1044
+ color: #9ca3af;
1045
+ font-size: 0.7rem;
1046
+ font-style: italic;
1047
+ opacity: 0.8;
1048
+ margin-left: auto;
1049
+ user-select: none;
1050
+ }
1051
+
1052
+ .groupHeaderContainer {
1053
+ display: flex;
1054
+ align-items: center;
1055
+ justify-content: space-between;
1056
+ width: 100%;
1057
+ height: 34px;
1058
+ padding: 6px 12px;
1059
+ background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
1060
+ border-left: 4px solid var(--primary-color, #3b82f6);
1061
+ position: relative;
1062
+ overflow: hidden;
1063
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
1064
+ border-radius: 0 4px 4px 0;
1065
+ cursor: pointer;
1066
+ box-sizing: border-box;
1067
+ line-height: 1.1;
1068
+ flex-shrink: 0;
1069
+ transition: background-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
1070
+
1071
+ &:hover {
1072
+ background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
1073
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
1074
+ transform: translateY(-1px);
1075
+
1076
+ &::after {
1077
+ opacity: 1;
1078
+ }
1079
+ }
1080
+
1081
+ &:active {
1082
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
1083
+ transform: translateY(0);
1084
+ }
1085
+
1086
+ &::after {
1087
+ content: '';
1088
+ position: absolute;
1089
+ top: 0;
1090
+ left: 0;
1091
+ right: 0;
1092
+ bottom: 0;
1093
+ background: rgba(59, 130, 246, 0.05);
1094
+ opacity: 0;
1095
+ transition: opacity 0.2s ease;
1096
+ pointer-events: none;
1097
+ }
1098
+ }
1099
+
1100
+ .groupHeaderPattern {
1101
+ position: absolute;
1102
+ top: 0;
1103
+ left: 0;
1104
+ right: 0;
1105
+ bottom: 0;
1106
+ background: repeating-linear-gradient(
1107
+ 45deg,
1108
+ transparent,
1109
+ transparent 2px,
1110
+ rgba(59, 130, 246, 0.03) 2px,
1111
+ rgba(59, 130, 246, 0.03) 4px
1112
+ );
1113
+ z-index: 1;
1114
+ pointer-events: none;
1115
+ }
1116
+
1117
+ // Touch tooltip styles
1118
+ .touchTooltip {
1119
+ animation: fadeInTooltip 0.2s ease-out;
1120
+ }
1121
+
1122
+ .touchTooltipContent {
1123
+ background: rgba(0, 0, 0, 0.9);
1124
+ color: white;
1125
+ padding: 8px 12px;
1126
+ border-radius: 6px;
1127
+ font-size: 0.875rem;
1128
+ font-weight: 500;
1129
+ white-space: nowrap;
1130
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
1131
+ position: relative;
1132
+
1133
+ // Arrow pointing down
1134
+ &::after {
1135
+ content: '';
1136
+ position: absolute;
1137
+ top: 100%;
1138
+ left: 50%;
1139
+ transform: translateX(-50%);
1140
+ border: 6px solid transparent;
1141
+ border-top-color: rgba(0, 0, 0, 0.9);
1142
+ }
1143
+ }
1144
+
1145
+ // Animations
1146
+ @keyframes fadeInTooltip {
1147
+ from {
1148
+ opacity: 0;
1149
+ transform: translateY(-5px);
1150
+ }
1151
+ to {
1152
+ opacity: 1;
1153
+ transform: translateY(0);
1154
+ }
1155
+ }
1156
+
1157
+ @keyframes touchPulse {
1158
+ 0%, 100% {
1159
+ opacity: 0.8;
1160
+ transform: scale(1);
1161
+ }
1162
+ 50% {
1163
+ opacity: 1;
1164
+ transform: scale(1.2);
1165
+ }
1166
+ }