@visns-studio/visns-components 5.2.3 → 5.2.5

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
@@ -78,7 +78,7 @@
78
78
  "react-dom": "^17.0.0 || ^18.0.0"
79
79
  },
80
80
  "name": "@visns-studio/visns-components",
81
- "version": "5.2.3",
81
+ "version": "5.2.5",
82
82
  "description": "Various packages to assist in the development of our Custom Applications.",
83
83
  "main": "src/index.js",
84
84
  "files": [
@@ -1,5 +1,4 @@
1
1
  import '../../styles/global.css';
2
-
3
2
  import React, { useState, useEffect, useRef } from 'react';
4
3
  import { Link, useNavigate } from 'react-router-dom';
5
4
  import { ResponsiveBar } from '@nivo/bar';
@@ -9,7 +8,7 @@ import Popup from 'reactjs-popup';
9
8
  import dayjs from 'dayjs';
10
9
  import parse from 'html-react-parser';
11
10
  import { createSwapy } from 'swapy';
12
- import { CircleX } from 'akar-icons';
11
+ import { CircleX, Minus, Plus, Star } from 'akar-icons';
13
12
 
14
13
  import CustomFetch from '../../crm/Fetch';
15
14
  import MultiSelect from '../../crm/MultiSelect';
@@ -18,29 +17,60 @@ import styles from './styles/GenericDashboard.module.scss';
18
17
 
19
18
  const toSnakeCase = (str) => str.replace(/[\s-]+/g, '_').toLowerCase();
20
19
 
21
- function GenericDashboard({ setting }) {
20
+ function GenericDashboard({ setting, userProfile }) {
22
21
  const swapy = useRef(null);
23
22
  const container = useRef(null);
24
23
  const navigate = useNavigate();
24
+ const [dashboardSetting, setDashboardSetting] = useState(
25
+ userProfile.dashboard_setting || setting
26
+ );
25
27
  const [data, setData] = useState({});
26
28
  const [dropdowns, setDropdowns] = useState({});
27
- const [filters, setFilters] = useState({}); // Track filter values for each widget
29
+ const [filters, setFilters] = useState({});
28
30
  const [modalShow, setModalShow] = useState(false);
29
31
  const [modalContent, setModalContent] = useState(null);
32
+ const [editMode, setEditMode] = useState(false);
33
+
34
+ const getDefaultDate = (defaultValue) => {
35
+ switch (defaultValue) {
36
+ case 'startOfMonth':
37
+ return dayjs().startOf('month').format('YYYY-MM-DD');
38
+ case 'endOfMonth':
39
+ return dayjs().endOf('month').format('YYYY-MM-DD');
40
+ default:
41
+ return '';
42
+ }
43
+ };
44
+
45
+ // Initialize filters based on each widget’s filters
46
+ useEffect(() => {
47
+ const initialFilters = {};
48
+ dashboardSetting.widgets.forEach((widget) => {
49
+ if (widget.filters) {
50
+ initialFilters[widget.id] = widget.filters.reduce(
51
+ (acc, filter) => {
52
+ if (filter.type === 'date' && filter.default) {
53
+ acc[filter.id] = getDefaultDate(filter.default);
54
+ } else if (filter.type === 'multi-dropdown-ajax') {
55
+ acc[filter.id] = [];
56
+ } else if (filter.type === 'years') {
57
+ acc[filter.id] = dayjs().year();
58
+ }
59
+ return acc;
60
+ },
61
+ {}
62
+ );
63
+ }
64
+ });
65
+ setFilters(initialFilters);
66
+ updateDashboardSetting(dashboardSetting);
67
+ }, [dashboardSetting]);
30
68
 
31
69
  const fetchData = async (appliedFilters = {}) => {
32
70
  try {
33
- // Flatten the nested widgets array into a single array
34
- const flattenedWidgets = setting.widgets.reduce(
35
- (acc, row) => [...acc, ...row],
36
- []
37
- );
38
-
39
- // Map through the flattened array of widgets
40
- const promises = flattenedWidgets.map(async (widget) => {
71
+ const promises = dashboardSetting.widgets.map(async (widget) => {
41
72
  const widgetFilters = appliedFilters[widget.id] || {};
42
73
 
43
- // Fetch dropdown data if required
44
74
  if (widget.filters) {
45
75
  for (const filter of widget.filters) {
46
76
  if (filter.url) {
@@ -68,7 +98,6 @@ function GenericDashboard({ setting }) {
68
98
  }
69
99
  }
70
100
 
71
- // Check if widget.api exists before making a request
72
101
  if (widget.api?.url && widget.api?.method) {
73
102
  return CustomFetch(
74
103
  widget.api.url,
@@ -76,15 +105,11 @@ function GenericDashboard({ setting }) {
76
105
  widgetFilters
77
106
  );
78
107
  }
79
-
80
- return null; // Return null if no API request is needed
108
+ return null;
81
109
  });
82
110
 
83
- // Execute all API requests
84
111
  const results = await Promise.all(promises);
85
-
86
- // Build a data object keyed by widget id
87
- const fetchedData = flattenedWidgets.reduce(
112
+ const fetchedData = dashboardSetting.widgets.reduce(
88
113
  (acc, widget, index) => {
89
114
  if (results[index]) {
90
115
  acc[widget.id] = results[index]?.data;
@@ -93,13 +118,16 @@ function GenericDashboard({ setting }) {
93
118
  },
94
119
  {}
95
120
  );
96
-
97
121
  setData(fetchedData);
98
122
  } catch (error) {
99
123
  console.error('Error fetching dashboard data:', error);
100
124
  }
101
125
  };
102
126
 
127
+ useEffect(() => {
128
+ fetchData(filters);
129
+ }, [filters]);
130
+
103
131
  const openModal = (widgetId) => {
104
132
  setModalContent(widgetId);
105
133
  setModalShow(true);
@@ -150,81 +178,80 @@ function GenericDashboard({ setting }) {
150
178
  }
151
179
  };
152
180
 
153
- const getDefaultDate = (defaultValue) => {
154
- switch (defaultValue) {
155
- case 'startOfMonth':
156
- return dayjs().startOf('month').format('YYYY-MM-DD');
157
- case 'endOfMonth':
158
- return dayjs().endOf('month').format('YYYY-MM-DD');
159
- default:
160
- return '';
181
+ const updateDashboardSetting = async (newSetting) => {
182
+ try {
183
+ if (userProfile.id && newSetting) {
184
+ await CustomFetch(`/ajax/users/${userProfile.id}/`, 'PUT', {
185
+ dashboard_setting: newSetting,
186
+ });
187
+ }
188
+ } catch (err) {
189
+ console.info(err);
161
190
  }
162
191
  };
163
192
 
164
- useEffect(() => {
165
- // Initialize filters with default values when settings change
166
- const initializeFilters = () => {
167
- const initialFilters = {};
168
-
169
- setting.widgets.forEach((row) => {
170
- row.forEach((widget) => {
171
- if (widget.filters) {
172
- initialFilters[widget.id] = widget.filters.reduce(
173
- (acc, filter) => {
174
- if (filter.type === 'date' && filter.default) {
175
- acc[filter.id] = getDefaultDate(
176
- filter.default
177
- );
178
- } else if (
179
- filter.type === 'multi-dropdown-ajax'
180
- ) {
181
- acc[filter.id] = [];
182
- } else if (filter.type === 'years') {
183
- acc[filter.id] = dayjs().year();
184
- }
185
- return acc;
186
- },
187
- {}
188
- );
193
+ // Cycle widget size: "1/3" → "half" → "2/3" → "full" (and reverse)
194
+ const changeSize = (widgetId, direction) => {
195
+ setDashboardSetting((prevSetting) => {
196
+ const updatedWidgets = prevSetting.widgets.map((widget) => {
197
+ if (widget.id === widgetId) {
198
+ const currentSize = widget.size || '1/3';
199
+ let newSize = currentSize;
200
+ const order = ['1/3', 'half', '2/3', 'full'];
201
+ const currentIndex = order.indexOf(currentSize);
202
+ if (
203
+ direction === 'increase' &&
204
+ currentIndex < order.length - 1
205
+ ) {
206
+ newSize = order[currentIndex + 1];
207
+ } else if (direction === 'decrease' && currentIndex > 0) {
208
+ newSize = order[currentIndex - 1];
189
209
  }
190
- });
191
- });
192
-
193
- setFilters(initialFilters);
194
- };
195
-
196
- initializeFilters();
197
- }, [setting]);
198
210
 
199
- useEffect(() => {
200
- fetchData(filters);
201
- }, [filters]);
202
-
203
- useEffect(() => {
204
- // If container element is loaded
205
- if (container.current) {
206
- swapy.current = createSwapy(container.current);
211
+ return { ...widget, size: newSize };
212
+ }
213
+ return widget;
214
+ });
215
+ const newSetting = { ...prevSetting, widgets: updatedWidgets };
216
+ updateDashboardSetting(newSetting);
217
+ return newSetting;
218
+ });
219
+ };
207
220
 
208
- // Your event listeners
209
- swapy.current.onSwap((event) => {
210
- console.log('swap', event);
221
+ // Toggle the highlight state of a widget
222
+ const toggleHighlight = (widgetId) => {
223
+ setDashboardSetting((prevSetting) => {
224
+ const updatedWidgets = prevSetting.widgets.map((widget) => {
225
+ if (widget.id === widgetId) {
226
+ return { ...widget, highlight: !widget.highlight };
227
+ }
228
+ return widget;
211
229
  });
212
- }
230
+ const newSetting = { ...prevSetting, widgets: updatedWidgets };
231
+ updateDashboardSetting(newSetting);
232
+ return newSetting;
233
+ });
234
+ };
213
235
 
214
- return () => {
215
- // Destroy the swapy instance on component destroy
216
- swapy.current?.destroy();
217
- };
218
- }, []);
236
+ // Map widget size to a CSS class
237
+ const getGridClass = (size) => {
238
+ switch (size) {
239
+ case 'full':
240
+ return styles.grid__full;
241
+ case 'half':
242
+ return styles.grid__half;
243
+ case '2/3':
244
+ return styles.grid__twothird;
245
+ case '1/3':
246
+ default:
247
+ return styles.grid__three;
248
+ }
249
+ };
219
250
 
220
251
  const renderFilters = (widget) => {
221
252
  if (!widget.filters || widget.filters.length === 0) return null;
222
-
223
253
  return (
224
254
  <div className={styles['filter-container']}>
225
- <div className={styles['filter-header']}>
226
- <h3>Filters</h3>
227
- </div>
228
255
  <div className={styles['filter-fields']}>
229
256
  {widget.filters.map((filter) => {
230
257
  switch (filter.type) {
@@ -257,7 +284,7 @@ function GenericDashboard({ setting }) {
257
284
  filter.id,
258
285
  e.target.value
259
286
  );
260
- e.target.blur(); // Closes the picker
287
+ e.target.blur();
261
288
  }}
262
289
  />
263
290
  </div>
@@ -328,7 +355,7 @@ function GenericDashboard({ setting }) {
328
355
  filter.id,
329
356
  e.target.value
330
357
  );
331
- e.target.blur(); // Closes the picker
358
+ e.target.blur();
332
359
  }}
333
360
  >
334
361
  {Array.from(
@@ -365,8 +392,7 @@ function GenericDashboard({ setting }) {
365
392
  ...prevFilters,
366
393
  [widget.id]: {},
367
394
  }));
368
-
369
- fetchData(); // Reset filters and fetch data
395
+ fetchData();
370
396
  }}
371
397
  >
372
398
  Clear Filters
@@ -379,7 +405,6 @@ function GenericDashboard({ setting }) {
379
405
 
380
406
  const renderWidget = (widget) => {
381
407
  const widgetData = data[widget.id] || [];
382
-
383
408
  if (
384
409
  widgetData.length > 0 ||
385
410
  widgetData?.data?.length > 0 ||
@@ -389,7 +414,13 @@ function GenericDashboard({ setting }) {
389
414
  case 'counter':
390
415
  return (
391
416
  <>
392
- <div className={styles.widgetNo}>
417
+ <div
418
+ className={`${
419
+ widget.highlight
420
+ ? styles['widgetNo-alt']
421
+ : styles.widgetNo
422
+ }`}
423
+ >
393
424
  <span>
394
425
  {(Array.isArray(widgetData) &&
395
426
  widgetData.length === 0) ||
@@ -422,12 +453,10 @@ function GenericDashboard({ setting }) {
422
453
  return (
423
454
  <ResponsivePie data={widgetData} {...widget.props} />
424
455
  );
425
- case 'bar':
456
+ case 'bar': {
426
457
  if (widgetData.keys && widget.props.keys) {
427
458
  widget.props.keys = widgetData.keys;
428
459
  }
429
-
430
- // Build legendMapping and attach legendLabel and a custom tooltip if a legend is provided.
431
460
  let legendMapping = {};
432
461
  if (widget.legend) {
433
462
  legendMapping = widget.legend.reduce((acc, curr) => {
@@ -437,7 +466,6 @@ function GenericDashboard({ setting }) {
437
466
  widget.props.legendLabel = (e) =>
438
467
  legendMapping[e.id] || e.id;
439
468
 
440
- // Define a custom tooltip that uses the legendMapping
441
469
  const CustomTooltip = ({ id, value, color }) => {
442
470
  const label = legendMapping[id] || id;
443
471
  return (
@@ -457,14 +485,13 @@ function GenericDashboard({ setting }) {
457
485
 
458
486
  widget.props.tooltip = CustomTooltip;
459
487
  }
460
-
461
488
  const barData =
462
489
  widgetData.bar?.data || widgetData.data || widgetData;
463
-
464
490
  if (barData?.length > 0) {
465
491
  return (
466
492
  <div style={{ height: widget.height || '600px' }}>
467
493
  <ResponsiveBar
494
+ key={`${widget.id}-${widget.size}`}
468
495
  data={barData}
469
496
  {...widget.props}
470
497
  />
@@ -472,15 +499,18 @@ function GenericDashboard({ setting }) {
472
499
  );
473
500
  }
474
501
  break;
502
+ }
475
503
  case 'line':
476
504
  if (widgetData.length > 0) {
477
505
  return (
478
506
  <ResponsiveLine
507
+ key={`${widget.id}-${widget.size}`}
479
508
  data={widgetData}
480
509
  {...widget.props}
481
510
  />
482
511
  );
483
512
  }
513
+ break;
484
514
  case 'list':
485
515
  return (
486
516
  <ul className={styles.dashList}>
@@ -520,10 +550,9 @@ function GenericDashboard({ setting }) {
520
550
  )}
521
551
  </ul>
522
552
  );
523
- case 'table':
553
+ case 'table': {
524
554
  const tableData = widgetData.data || widgetData;
525
555
  const tableKeys = widgetData.keys || [];
526
-
527
556
  return (
528
557
  <div className={styles['table-container']}>
529
558
  {tableData.length > 0 ? (
@@ -581,14 +610,13 @@ function GenericDashboard({ setting }) {
581
610
  {tableKeys && tableKeys.length > 0
582
611
  ? tableKeys.map(
583
612
  (type, typeIndex) => {
584
- let total = 0; // Track total for the current form type
613
+ let total = 0;
585
614
  return (
586
615
  <tr
587
616
  key={`form-type-${toSnakeCase(
588
617
  type
589
618
  )}`}
590
619
  >
591
- {/* <td>{type}</td> */}
592
620
  {widget.total
593
621
  .column &&
594
622
  tableData.map(
@@ -608,7 +636,6 @@ function GenericDashboard({ setting }) {
608
636
  widget
609
637
  .axisKeys
610
638
  .xAxis;
611
-
612
639
  const column =
613
640
  row[
614
641
  columnKey
@@ -627,7 +654,6 @@ function GenericDashboard({ setting }) {
627
654
  : 0;
628
655
  total +=
629
656
  count;
630
-
631
657
  return (
632
658
  <td
633
659
  key={`${toSnakeCase(
@@ -680,8 +706,6 @@ function GenericDashboard({ setting }) {
680
706
  </tr>
681
707
  )
682
708
  )}
683
-
684
- {/* Add a total row for supervisors */}
685
709
  {widget.total.column && (
686
710
  <tr>
687
711
  <td>
@@ -707,7 +731,6 @@ function GenericDashboard({ setting }) {
707
731
  column.form_template_label ===
708
732
  type
709
733
  );
710
-
711
734
  return (
712
735
  sum +
713
736
  (column
@@ -721,7 +744,6 @@ function GenericDashboard({ setting }) {
721
744
  },
722
745
  0
723
746
  );
724
-
725
747
  return (
726
748
  <td
727
749
  key={`${toSnakeCase(
@@ -752,6 +774,7 @@ function GenericDashboard({ setting }) {
752
774
  )}
753
775
  </div>
754
776
  );
777
+ }
755
778
  default:
756
779
  return null;
757
780
  }
@@ -760,87 +783,178 @@ function GenericDashboard({ setting }) {
760
783
  }
761
784
  };
762
785
 
763
- const renderRows = () => {
764
- return setting.widgets.map((row, rowIndex) => {
765
- // Use the number of widgets in the row to determine the grid class
766
- const columns = row.length;
767
- const gridClass =
768
- columns === 1
769
- ? styles.grid__full
770
- : columns === 2
771
- ? styles.grid__half
772
- : columns === 3
773
- ? styles.grid__three
774
- : styles.grid__three; // fallback if more than 3 columns
786
+ // Drag-and-drop swap handling. Each widget is given a slot based on its index.
787
+ const handleWidgetSwap = (event) => {
788
+ const parseSlot = (slot) => {
789
+ // Slot format: "widget-slot-INDEX"
790
+ const parts = slot.split('-');
791
+ return parseInt(parts[2], 10);
792
+ };
793
+
794
+ const fromIndex = parseSlot(event.fromSlot);
795
+ const toIndex = parseSlot(event.toSlot);
796
+
797
+ setDashboardSetting((prevSetting) => {
798
+ const newWidgets = [...prevSetting.widgets];
799
+ [newWidgets[fromIndex], newWidgets[toIndex]] = [
800
+ newWidgets[toIndex],
801
+ newWidgets[fromIndex],
802
+ ];
803
+ const newSetting = { ...prevSetting, widgets: newWidgets };
804
+ updateDashboardSetting(newSetting);
805
+ return newSetting;
806
+ });
807
+ };
808
+
809
+ useEffect(() => {
810
+ if (editMode && container.current) {
811
+ swapy.current = createSwapy(container.current, {
812
+ manualSwap: true,
813
+ });
814
+ swapy.current.onSwap((event) => {
815
+ console.log('swap event', event);
816
+ handleWidgetSwap(event);
817
+ });
818
+ }
819
+ return () => {
820
+ swapy.current?.destroy();
821
+ swapy.current = null;
822
+ };
823
+ }, [editMode]);
775
824
 
825
+ const renderWidgets = () => {
826
+ return dashboardSetting.widgets.map((widget, index) => {
776
827
  return (
777
- <div className={styles.grid__row} key={`row-${rowIndex}`}>
778
- {row.map((widget) => (
779
- <div
780
- key={widget.id}
781
- className={`${gridClass} ${
782
- widget.type !== 'list'
783
- ? widget.highlight
784
- ? styles['dashwidget--highlight']
785
- : styles.dashwidget
786
- : ''
787
- }`}
788
- data-swapy-slot={`row-slot-${widget.id}-${rowIndex}`}
789
- >
790
- <div data-swapy-item={`widget-${widget.id}`}>
791
- <div className={styles.widgetTitle}>
792
- <h2>{widget.title}</h2>
793
- </div>
794
- {renderFilters(widget)}
795
- <div>{renderWidget(widget)}</div>
796
- {widget.modal && (
797
- <div className={styles.widgetAction}>
828
+ <div
829
+ key={widget.id}
830
+ className={`${getGridClass(widget.size)} ${
831
+ widget.type !== 'list'
832
+ ? widget.highlight
833
+ ? styles['dashwidget--highlight']
834
+ : styles.dashwidget
835
+ : ''
836
+ } ${
837
+ editMode
838
+ ? widget.highlight
839
+ ? styles.editModeWidgetHighlight
840
+ : styles.editModeWidgetNormal
841
+ : ''
842
+ }`}
843
+ style={editMode ? { cursor: 'grab' } : {}}
844
+ {...(editMode && {
845
+ 'data-swapy-slot': `widget-slot-${index}`,
846
+ })}
847
+ >
848
+ <div
849
+ {...(editMode && {
850
+ 'data-swapy-item': `widget-${widget.id}`,
851
+ })}
852
+ className={`${styles.widgetItem}`}
853
+ >
854
+ <div className={styles.widgetTitle}>
855
+ <h2>{widget.title}</h2>
856
+ </div>
857
+ {renderFilters(widget)}
858
+ <div>{renderWidget(widget)}</div>
859
+ {widget.modal && (
860
+ <div className={styles.widgetAction}>
861
+ <button
862
+ className={`${styles.btn} ${styles.dmore}`}
863
+ onClick={() => openModal(widget.id)}
864
+ >
865
+ View all{' '}
866
+ <span>
867
+ ({data[widget.id]?.length || 0})
868
+ </span>
869
+ </button>
870
+ </div>
871
+ )}
872
+ {editMode && (
873
+ <div className={styles.widgetTools}>
874
+ <div className={styles.resizeTools}>
875
+ <button
876
+ onClick={() =>
877
+ toggleHighlight(widget.id)
878
+ }
879
+ >
880
+ <Star strokeWidth={2} size={12} />
881
+ </button>
882
+ {(widget.size || '1/3') !== '1/3' && (
798
883
  <button
799
- className={`${styles.btn} ${styles.dmore}`}
800
- onClick={() => openModal(widget.id)}
884
+ onClick={() =>
885
+ changeSize(
886
+ widget.id,
887
+ 'decrease'
888
+ )
889
+ }
801
890
  >
802
- View all{' '}
803
- <span>
804
- ({data[widget.id]?.length || 0})
805
- </span>
891
+ <Minus strokeWidth={2} size={12} />
806
892
  </button>
807
- </div>
808
- )}
893
+ )}
894
+ {(widget.size || '1/3') !== 'full' && (
895
+ <button
896
+ onClick={() =>
897
+ changeSize(
898
+ widget.id,
899
+ 'increase'
900
+ )
901
+ }
902
+ >
903
+ <Plus strokeWidth={2} size={12} />
904
+ </button>
905
+ )}
906
+ </div>
809
907
  </div>
810
- </div>
811
- ))}
908
+ )}
909
+ </div>
812
910
  </div>
813
911
  );
814
912
  });
815
913
  };
816
914
 
817
915
  return (
818
- <div className={styles.grid} ref={container}>
819
- {renderRows()}
820
- <Popup open={modalShow} onClose={closeModal}>
821
- <div className="modalwrap top--modal modalWide">
822
- <div className="modal">
823
- <div className="modal__header">
824
- <h1>{setting.modalTitles?.[modalContent]}</h1>
825
- <button
826
- className="modal__close"
827
- onClick={closeModal}
828
- >
829
- <CircleX strokeWidth={1} size={24} />
830
- </button>
831
- </div>
832
- <div className="modal__content">
833
- {modalContent &&
834
- renderWidget(
835
- setting.widgets.find(
836
- (w) => w.id === modalContent
837
- )
838
- )}
916
+ <>
917
+ <div className={styles.editButtonContainer}>
918
+ <button
919
+ className={styles.btn}
920
+ onClick={() => setEditMode((prev) => !prev)}
921
+ >
922
+ {editMode ? 'Done' : 'Edit'}
923
+ </button>
924
+ </div>
925
+ <div className={styles.grid} ref={container}>
926
+ {renderWidgets()}
927
+ <Popup open={modalShow} onClose={closeModal}>
928
+ <div className="modalwrap top--modal modalWide">
929
+ <div className="modal">
930
+ <div className="modal__header">
931
+ <h1>
932
+ {
933
+ dashboardSetting.modalTitles?.[
934
+ modalContent
935
+ ]
936
+ }
937
+ </h1>
938
+ <button
939
+ className="modal__close"
940
+ onClick={closeModal}
941
+ >
942
+ <CircleX strokeWidth={1} size={24} />
943
+ </button>
944
+ </div>
945
+ <div className="modal__content">
946
+ {modalContent &&
947
+ renderWidget(
948
+ dashboardSetting.widgets.find(
949
+ (w) => w.id === modalContent
950
+ )
951
+ )}
952
+ </div>
839
953
  </div>
840
954
  </div>
841
- </div>
842
- </Popup>
843
- </div>
955
+ </Popup>
956
+ </div>
957
+ </>
844
958
  );
845
959
  }
846
960
 
@@ -1,14 +1,12 @@
1
- .grid__row {
2
- width: 100%;
1
+ /* New grid container */
2
+ .grid {
3
3
  display: flex;
4
- align-items: flex-start;
5
- flex-wrap: nowrap;
6
- height: auto;
4
+ flex-wrap: wrap;
7
5
  gap: 1rem;
8
6
  }
9
7
 
10
8
  .grid__three {
11
- width: 33%;
9
+ flex: 0 0 calc(33.33% - 0.67rem);
12
10
  background: var(--item-color);
13
11
  border-radius: var(--br);
14
12
  border: 1px solid rgba(var(--item-color-rgb), 1.15);
@@ -21,10 +19,10 @@
21
19
  }
22
20
 
23
21
  .grid__half {
24
- width: 50%;
22
+ flex: 0 0 calc(50% - 0.5rem);
25
23
  background: var(--item-color);
26
- border-radius: 6px;
27
- border: 1px solid #dadce0;
24
+ border-radius: var(--br);
25
+ border: 1px solid rgba(var(--item-color-rgb), 1.15);
28
26
  box-sizing: border-box;
29
27
  overflow: visible;
30
28
  position: relative;
@@ -34,23 +32,23 @@
34
32
  }
35
33
 
36
34
  .grid__full {
37
- width: 100%;
35
+ flex: 0 0 100%;
38
36
  background: var(--item-color);
39
37
  border-radius: var(--br);
40
38
  border: 1px solid rgba(var(--item-color-rgb), 1.15);
41
39
  box-sizing: border-box;
42
40
  overflow: visible;
43
41
  position: relative;
44
- padding: 10px;
42
+ padding: 20px;
45
43
  margin: 8px 0;
46
44
  box-shadow: 0 4px 0 rgba(var(--primary-rgb), 0.05);
47
45
  }
48
46
 
49
- .grid__dashfull {
50
- width: 100%;
47
+ .grid__twothird {
48
+ flex: 0 0 calc(66.66% - 0.67rem);
51
49
  background: var(--item-color);
52
- border-radius: 6px;
53
- border: 1px solid #dadce0;
50
+ border-radius: var(--br);
51
+ border: 1px solid rgba(var(--item-color-rgb), 1.15);
54
52
  box-sizing: border-box;
55
53
  overflow: visible;
56
54
  position: relative;
@@ -59,21 +57,21 @@
59
57
  box-shadow: 0 4px 0 rgba(var(--primary-rgb), 0.05);
60
58
  }
61
59
 
60
+ /* The remaining CSS remains largely unchanged */
62
61
  .widgetTitle {
63
62
  width: 100%;
64
63
  height: auto;
65
64
  padding: 0;
66
65
  margin-bottom: 0.85rem;
67
-
68
- h2 {
69
- font-size: 1rem;
70
- margin: 0;
71
- padding: 0;
72
- line-height: 1;
73
- text-transform: capitalize;
74
- text-align: left;
75
- color: var(--primary-color);
76
- }
66
+ }
67
+ .widgetTitle h2 {
68
+ font-size: 1rem;
69
+ margin: 0;
70
+ padding: 0;
71
+ line-height: 1;
72
+ text-transform: capitalize;
73
+ text-align: left;
74
+ color: var(--primary-color);
77
75
  }
78
76
 
79
77
  .dashList {
@@ -192,6 +190,22 @@
192
190
  padding: 0 0 2rem 0;
193
191
  box-sizing: border-box;
194
192
 
193
+ span {
194
+ width: 100%;
195
+ display: block;
196
+ text-align: center;
197
+ font-weight: 400;
198
+ font-size: 3em;
199
+ color: var(--primary-color);
200
+ }
201
+ }
202
+
203
+ .widgetNo-alt {
204
+ width: 100%;
205
+ height: auto;
206
+ padding: 0 0 2rem 0;
207
+ box-sizing: border-box;
208
+
195
209
  span {
196
210
  width: 100%;
197
211
  display: block;
@@ -212,11 +226,12 @@
212
226
  display: block;
213
227
  padding: 0.25em 3em;
214
228
  background: none;
215
- border-color: rgba(var(--tertiary-color-rgb), 0.05);
229
+ border-color: rgba(var(--primary-rgb), 0.05);
230
+ border-radius: 5px;
216
231
 
217
232
  &:hover {
218
233
  background: none;
219
- color: var(--tertiary-color);
234
+ border-color: rgba(var(--primary-rgb), 0.5);
220
235
  }
221
236
  }
222
237
 
@@ -430,3 +445,94 @@
430
445
  .table-container th:last-child {
431
446
  border-right: none;
432
447
  }
448
+
449
+ // Indicates that the widget is in edit mode with a glowing dashed border.
450
+ .editModeWidget {
451
+ border: 2px dashed var(--primary-color);
452
+ box-shadow: 0 0 10px var(--primary-color);
453
+ }
454
+
455
+ // Container for the edit mode toggle button.
456
+ .editButtonContainer {
457
+ position: fixed;
458
+ bottom: 1rem;
459
+ right: 1rem;
460
+ z-index: 1000; // Ensures the button stays on top of other elements
461
+ }
462
+
463
+ .editButtonContainer .btn {
464
+ padding: 0.5rem 1rem;
465
+ background: var(--primary-color);
466
+ color: var(--tertiary-color);
467
+ border: none;
468
+ border-radius: var(--br);
469
+ font-size: 0.875rem;
470
+ font-weight: 600;
471
+ text-transform: uppercase;
472
+ cursor: pointer;
473
+ transition: background-color 0.25s ease;
474
+ }
475
+
476
+ .editButtonContainer .btn:hover {
477
+ background-color: var(--secondary-color);
478
+ }
479
+
480
+ // Normal edit mode style for non-highlighted widgets
481
+ .editModeWidgetNormal {
482
+ border: 2px dashed var(--primary-color);
483
+ box-shadow: 0 0 10px var(--primary-color);
484
+
485
+ // Use the default resize tools positioning for normal widgets.
486
+ .resizeTools {
487
+ top: -5px;
488
+ right: 0px;
489
+ }
490
+ }
491
+
492
+ // Edit mode style for highlighted widgets (using a contrasting color)
493
+ .editModeWidgetHighlight {
494
+ border: 2px dashed var(--secondary-color);
495
+ box-shadow: 0 0 10px var(--secondary-color);
496
+
497
+ // Position resize tools further up to avoid blending with the background.
498
+ .resizeTools {
499
+ top: -5px; // Adjust this value as needed
500
+ right: 0px;
501
+ }
502
+ }
503
+
504
+ // Resizing tool container (common styling)
505
+ .resizeTools {
506
+ position: absolute;
507
+ display: flex;
508
+ gap: 5px;
509
+
510
+ button {
511
+ background: var(--primary-color);
512
+ color: var(--tertiary-color);
513
+ border: none;
514
+ border-radius: 3px;
515
+ padding: 0.25rem 0.5rem;
516
+ cursor: pointer;
517
+ font-size: 0.75rem;
518
+ transition: background-color 0.2s;
519
+
520
+ &:hover {
521
+ background-color: rgba(var(--primary-rgb), 0.8);
522
+ }
523
+ }
524
+ }
525
+
526
+ // Override the resize button for highlighted widgets
527
+ .editModeWidgetHighlight .resizeTools button {
528
+ background: var(--tertiary-color); // Use a contrasting background
529
+ color: var(--primary-color); // Ensure the text is legible
530
+
531
+ &:hover {
532
+ background-color: rgba(var(--tertiary-color-rgb), 0.8);
533
+ }
534
+ }
535
+
536
+ .widgetItem {
537
+ position: relative;
538
+ }