astro-tractstack 2.0.1 → 2.0.2
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 +1 -1
- package/templates/src/components/codehooks/EpinetDurationSelector.tsx +98 -179
- package/templates/src/components/codehooks/EpinetTableView.tsx +100 -75
- package/templates/src/components/codehooks/EpinetWrapper.tsx +71 -112
- package/templates/src/components/codehooks/SankeyDiagram.tsx +2 -2
- package/templates/src/components/storykeep/Dashboard_Analytics.tsx +37 -1
- package/templates/src/components/storykeep/state/FetchAnalytics.tsx +21 -70
- package/templates/src/stores/analytics.ts +14 -0
package/package.json
CHANGED
|
@@ -4,12 +4,14 @@ import { createListCollection } from '@ark-ui/react/collection';
|
|
|
4
4
|
import { Select } from '@ark-ui/react/select';
|
|
5
5
|
import { RadioGroup } from '@ark-ui/react/radio-group';
|
|
6
6
|
import { Portal } from '@ark-ui/react/portal';
|
|
7
|
+
import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon';
|
|
7
8
|
import CheckCircleIcon from '@heroicons/react/24/outline/CheckCircleIcon';
|
|
8
9
|
import ChevronLeftIcon from '@heroicons/react/24/outline/ChevronLeftIcon';
|
|
9
10
|
import ChevronRightIcon from '@heroicons/react/24/outline/ChevronRightIcon';
|
|
10
11
|
import { epinetCustomFilters } from '@/stores/analytics';
|
|
11
12
|
import EpinetTableView from './EpinetTableView';
|
|
12
13
|
import { MAX_ANALYTICS_HOURS } from '@/constants';
|
|
14
|
+
import type { AppliedFilter } from '@/stores/analytics';
|
|
13
15
|
|
|
14
16
|
const getHourOptions = () => {
|
|
15
17
|
const hours = Array.from({ length: 24 }, (_, i) => ({
|
|
@@ -25,7 +27,7 @@ const getHourOptions = () => {
|
|
|
25
27
|
const getEndHourOptions = () => {
|
|
26
28
|
const hours = Array.from({ length: 24 }, (_, i) => ({
|
|
27
29
|
value: i.toString().padStart(2, '0'),
|
|
28
|
-
label: `${i.toString().padStart(2, '0')}:59`,
|
|
30
|
+
label: `${i.toString().padStart(2, '0')}:59`,
|
|
29
31
|
sortOrder: i,
|
|
30
32
|
}));
|
|
31
33
|
hours.push({ value: '24', label: '23:59', sortOrder: 24 });
|
|
@@ -42,17 +44,28 @@ interface ContentMapItem {
|
|
|
42
44
|
type: string;
|
|
43
45
|
}
|
|
44
46
|
|
|
47
|
+
interface AvailableFilter {
|
|
48
|
+
beliefSlug: string;
|
|
49
|
+
values: string[];
|
|
50
|
+
}
|
|
51
|
+
|
|
45
52
|
interface EpinetDurationSelectorProps {
|
|
46
53
|
fullContentMap?: ContentMapItem[];
|
|
47
54
|
isLoading?: boolean;
|
|
48
55
|
hourlyNodeActivity?: any;
|
|
56
|
+
availableFilters?: AvailableFilter[];
|
|
57
|
+
appliedFilters?: AppliedFilter[];
|
|
58
|
+
onBeliefFilterChange: (beliefSlug: string, value: string) => void;
|
|
49
59
|
}
|
|
50
60
|
|
|
51
61
|
const EpinetDurationSelector = ({
|
|
52
62
|
fullContentMap,
|
|
53
63
|
isLoading,
|
|
54
64
|
hourlyNodeActivity,
|
|
55
|
-
|
|
65
|
+
availableFilters = [],
|
|
66
|
+
appliedFilters = [],
|
|
67
|
+
onBeliefFilterChange,
|
|
68
|
+
}: EpinetDurationSelectorProps) => {
|
|
56
69
|
const [startDate, setStartDate] = useState<Date | null>(null);
|
|
57
70
|
const [endDate, setEndDate] = useState<Date | null>(null);
|
|
58
71
|
const [isDatePickerOpen, setIsDatePickerOpen] = useState(false);
|
|
@@ -78,7 +91,7 @@ const EpinetDurationSelector = ({
|
|
|
78
91
|
const now = new Date();
|
|
79
92
|
const endDate = new Date(now);
|
|
80
93
|
let startDate = new Date(endDate);
|
|
81
|
-
startDate.setDate(startDate.getDate() - 7);
|
|
94
|
+
startDate.setDate(startDate.getDate() - 7);
|
|
82
95
|
|
|
83
96
|
setStartDate(startDate);
|
|
84
97
|
setEndDate(endDate);
|
|
@@ -90,13 +103,11 @@ const EpinetDurationSelector = ({
|
|
|
90
103
|
}));
|
|
91
104
|
}, []);
|
|
92
105
|
|
|
93
|
-
// MODIFIED: Enhanced sync from store
|
|
94
106
|
useEffect(() => {
|
|
95
107
|
if ($epinetCustomFilters.startTimeUTC && $epinetCustomFilters.endTimeUTC) {
|
|
96
108
|
const startUTC = new Date($epinetCustomFilters.startTimeUTC);
|
|
97
109
|
const endUTC = new Date($epinetCustomFilters.endTimeUTC);
|
|
98
110
|
|
|
99
|
-
// Update local dates and hours to match store
|
|
100
111
|
setStartDate(
|
|
101
112
|
new Date(
|
|
102
113
|
startUTC.getFullYear(),
|
|
@@ -124,20 +135,24 @@ const EpinetDurationSelector = ({
|
|
|
124
135
|
selectedUserId: $epinetCustomFilters.selectedUserId,
|
|
125
136
|
}));
|
|
126
137
|
}
|
|
127
|
-
}, [
|
|
138
|
+
}, [
|
|
139
|
+
$epinetCustomFilters.startTimeUTC,
|
|
140
|
+
$epinetCustomFilters.endTimeUTC,
|
|
141
|
+
$epinetCustomFilters.visitorType,
|
|
142
|
+
$epinetCustomFilters.selectedUserId,
|
|
143
|
+
]);
|
|
128
144
|
|
|
129
145
|
useEffect(() => {
|
|
130
146
|
setCurrentUserPage(0);
|
|
131
147
|
}, [localFilters.visitorType]);
|
|
132
148
|
|
|
133
|
-
// Handle UnsavedChangesBar-style visibility animations
|
|
134
149
|
useEffect(() => {
|
|
135
150
|
if (hasLocalChanges && !isVisible) {
|
|
136
151
|
setIsVisible(true);
|
|
137
|
-
setTimeout(() => setIsAnimating(true), 10);
|
|
152
|
+
setTimeout(() => setIsAnimating(true), 10);
|
|
138
153
|
} else if (!hasLocalChanges && isVisible && !isApplying) {
|
|
139
154
|
setIsAnimating(false);
|
|
140
|
-
setTimeout(() => setIsVisible(false), 300);
|
|
155
|
+
setTimeout(() => setIsVisible(false), 300);
|
|
141
156
|
}
|
|
142
157
|
}, [hasLocalChanges, isVisible, isApplying]);
|
|
143
158
|
|
|
@@ -167,14 +182,11 @@ const EpinetDurationSelector = ({
|
|
|
167
182
|
setErrorMessage(null);
|
|
168
183
|
};
|
|
169
184
|
|
|
170
|
-
// Create UTC datetime from local date and hour selection
|
|
171
185
|
const createUTCDateTime = (date: Date, hourStr: string): Date => {
|
|
172
186
|
const localDateTime = new Date(date);
|
|
173
187
|
const hour = hourStr === '24' ? 23 : parseInt(hourStr);
|
|
174
188
|
const minute = hourStr === '24' ? 59 : 0;
|
|
175
189
|
localDateTime.setHours(hour, minute, 0, 0);
|
|
176
|
-
|
|
177
|
-
// Convert to UTC by creating new Date with UTC values
|
|
178
190
|
return new Date(localDateTime.getTime());
|
|
179
191
|
};
|
|
180
192
|
|
|
@@ -183,26 +195,19 @@ const EpinetDurationSelector = ({
|
|
|
183
195
|
setErrorMessage('Please select both start and end dates.');
|
|
184
196
|
return;
|
|
185
197
|
}
|
|
186
|
-
|
|
187
198
|
const startUTCTime = createUTCDateTime(startDate, localFilters.startHour);
|
|
188
199
|
const endUTCTime = createUTCDateTime(endDate, localFilters.endHour);
|
|
189
|
-
|
|
190
200
|
if (endUTCTime < startUTCTime) {
|
|
191
201
|
setErrorMessage('End time must be after start time.');
|
|
192
202
|
return;
|
|
193
203
|
}
|
|
194
|
-
|
|
195
204
|
const nowUTC = new Date();
|
|
196
|
-
|
|
197
205
|
if (endUTCTime > nowUTC) {
|
|
198
206
|
setErrorMessage('End time cannot be in the future.');
|
|
199
207
|
return;
|
|
200
208
|
}
|
|
201
|
-
|
|
202
209
|
setIsApplying(true);
|
|
203
|
-
|
|
204
210
|
try {
|
|
205
|
-
// Update with UTC timestamps
|
|
206
211
|
epinetCustomFilters.set(window.TRACTSTACK_CONFIG?.tenantId || 'default', {
|
|
207
212
|
...$epinetCustomFilters,
|
|
208
213
|
visitorType: localFilters.visitorType,
|
|
@@ -210,11 +215,8 @@ const EpinetDurationSelector = ({
|
|
|
210
215
|
startTimeUTC: startUTCTime.toISOString(),
|
|
211
216
|
endTimeUTC: endUTCTime.toISOString(),
|
|
212
217
|
});
|
|
213
|
-
|
|
214
218
|
setHasLocalChanges(false);
|
|
215
219
|
setErrorMessage(null);
|
|
216
|
-
|
|
217
|
-
// Brief delay to show success state
|
|
218
220
|
setTimeout(() => {
|
|
219
221
|
setIsApplying(false);
|
|
220
222
|
}, 1000);
|
|
@@ -232,31 +234,26 @@ const EpinetDurationSelector = ({
|
|
|
232
234
|
setErrorMessage('Please select a valid date.');
|
|
233
235
|
return;
|
|
234
236
|
}
|
|
235
|
-
|
|
236
237
|
const newDate = new Date(
|
|
237
238
|
parseInt(dateValue.split('-')[0]),
|
|
238
239
|
parseInt(dateValue.split('-')[1]) - 1,
|
|
239
240
|
parseInt(dateValue.split('-')[2])
|
|
240
241
|
);
|
|
241
|
-
|
|
242
242
|
const nowUTC = new Date();
|
|
243
243
|
const maxPastTime = new Date(
|
|
244
244
|
nowUTC.getTime() - MAX_ANALYTICS_HOURS * 60 * 60 * 1000
|
|
245
245
|
);
|
|
246
|
-
|
|
247
246
|
if (newDate < maxPastTime) {
|
|
248
247
|
setErrorMessage(
|
|
249
248
|
`Date cannot be more than ${MAX_ANALYTICS_HOURS} hours in the past.`
|
|
250
249
|
);
|
|
251
250
|
return;
|
|
252
251
|
}
|
|
253
|
-
|
|
254
252
|
if (type === 'start') {
|
|
255
253
|
setStartDate(newDate);
|
|
256
254
|
} else {
|
|
257
255
|
setEndDate(newDate);
|
|
258
256
|
}
|
|
259
|
-
|
|
260
257
|
setHasLocalChanges(true);
|
|
261
258
|
setErrorMessage(null);
|
|
262
259
|
};
|
|
@@ -276,44 +273,23 @@ const EpinetDurationSelector = ({
|
|
|
276
273
|
isEndTime = false
|
|
277
274
|
) => {
|
|
278
275
|
if (!date) return '';
|
|
279
|
-
|
|
280
276
|
const localDateTime = new Date(date);
|
|
281
277
|
const hour = hourStr === '24' ? 23 : parseInt(hourStr);
|
|
282
|
-
// For end times, always show :59 to represent the full hour
|
|
283
278
|
const minute = isEndTime ? 59 : hourStr === '24' ? 59 : 0;
|
|
284
279
|
localDateTime.setHours(hour, minute, 0, 0);
|
|
285
|
-
|
|
286
280
|
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
287
|
-
|
|
288
|
-
return (
|
|
289
|
-
localDateTime.toLocaleString(undefined, {
|
|
290
|
-
year: 'numeric',
|
|
291
|
-
month: '2-digit',
|
|
292
|
-
day: '2-digit',
|
|
293
|
-
hour: '2-digit',
|
|
294
|
-
minute: '2-digit',
|
|
295
|
-
hour12: true,
|
|
296
|
-
timeZone,
|
|
297
|
-
}) + ` (${timeZone})`
|
|
298
|
-
);
|
|
281
|
+
return `${localDateTime.toLocaleString(undefined, { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: true, timeZone })} (${timeZone})`;
|
|
299
282
|
};
|
|
300
283
|
|
|
301
|
-
// Display current UTC range in local timezone with proper end time formatting
|
|
302
284
|
const formatCurrentUTCRange = () => {
|
|
303
285
|
const { startTimeUTC, endTimeUTC } = $epinetCustomFilters;
|
|
304
286
|
if (!startTimeUTC || !endTimeUTC) return '';
|
|
305
|
-
|
|
306
287
|
const startLocal = new Date(startTimeUTC);
|
|
307
288
|
const endLocal = new Date(endTimeUTC);
|
|
308
289
|
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
309
|
-
|
|
310
290
|
const formatTime = (date: Date, isEndTime = false) => {
|
|
311
|
-
// For end times, always show :59 for presentation
|
|
312
291
|
const displayDate = isEndTime ? new Date(date.getTime()) : date;
|
|
313
|
-
if (isEndTime)
|
|
314
|
-
displayDate.setMinutes(59);
|
|
315
|
-
}
|
|
316
|
-
|
|
292
|
+
if (isEndTime) displayDate.setMinutes(59);
|
|
317
293
|
return displayDate.toLocaleString(undefined, {
|
|
318
294
|
year: 'numeric',
|
|
319
295
|
month: '2-digit',
|
|
@@ -324,7 +300,6 @@ const EpinetDurationSelector = ({
|
|
|
324
300
|
timeZone,
|
|
325
301
|
});
|
|
326
302
|
};
|
|
327
|
-
|
|
328
303
|
return `${formatTime(startLocal, false)} to ${formatTime(endLocal, true)} (${timeZone})`;
|
|
329
304
|
};
|
|
330
305
|
|
|
@@ -332,20 +307,16 @@ const EpinetDurationSelector = ({
|
|
|
332
307
|
currentUserPage * usersPerPage,
|
|
333
308
|
(currentUserPage + 1) * usersPerPage
|
|
334
309
|
);
|
|
335
|
-
|
|
336
310
|
const totalUserPages = Math.ceil(
|
|
337
311
|
(($epinetCustomFilters.userCounts || []).length || 0) / usersPerPage
|
|
338
312
|
);
|
|
339
|
-
|
|
340
313
|
const nextUserPage = () => {
|
|
341
314
|
if (currentUserPage < totalUserPages - 1)
|
|
342
315
|
setCurrentUserPage(currentUserPage + 1);
|
|
343
316
|
};
|
|
344
|
-
|
|
345
317
|
const prevUserPage = () => {
|
|
346
318
|
if (currentUserPage > 0) setCurrentUserPage(currentUserPage - 1);
|
|
347
319
|
};
|
|
348
|
-
|
|
349
320
|
const maxDate = new Date();
|
|
350
321
|
const minDate = new Date();
|
|
351
322
|
minDate.setHours(minDate.getHours() - MAX_ANALYTICS_HOURS);
|
|
@@ -353,25 +324,15 @@ const EpinetDurationSelector = ({
|
|
|
353
324
|
const setPresetDateRange = (period: string) => {
|
|
354
325
|
const nowUTC = new Date();
|
|
355
326
|
let hoursBack: number;
|
|
356
|
-
|
|
357
|
-
if (period === '
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
hoursBack = 168;
|
|
361
|
-
} else if (period === '28d') {
|
|
362
|
-
hoursBack = 672;
|
|
363
|
-
} else {
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
366
|
-
|
|
327
|
+
if (period === '24h') hoursBack = 24;
|
|
328
|
+
else if (period === '7d') hoursBack = 168;
|
|
329
|
+
else if (period === '28d') hoursBack = 672;
|
|
330
|
+
else return;
|
|
367
331
|
const startTimeUTC = new Date(
|
|
368
332
|
nowUTC.getTime() - hoursBack * 60 * 60 * 1000
|
|
369
333
|
);
|
|
370
|
-
|
|
371
|
-
// Convert to local dates and set local state
|
|
372
334
|
const startLocal = new Date(startTimeUTC);
|
|
373
335
|
const endLocal = new Date(nowUTC);
|
|
374
|
-
|
|
375
336
|
setStartDate(
|
|
376
337
|
new Date(
|
|
377
338
|
startLocal.getFullYear(),
|
|
@@ -382,13 +343,11 @@ const EpinetDurationSelector = ({
|
|
|
382
343
|
setEndDate(
|
|
383
344
|
new Date(endLocal.getFullYear(), endLocal.getMonth(), endLocal.getDate())
|
|
384
345
|
);
|
|
385
|
-
|
|
386
346
|
setLocalFilters((prev) => ({
|
|
387
347
|
...prev,
|
|
388
348
|
startHour: startLocal.getHours().toString().padStart(2, '0'),
|
|
389
349
|
endHour: endLocal.getHours().toString().padStart(2, '0'),
|
|
390
350
|
}));
|
|
391
|
-
|
|
392
351
|
setHasLocalChanges(true);
|
|
393
352
|
setIsDatePickerOpen(false);
|
|
394
353
|
setErrorMessage(null);
|
|
@@ -402,11 +361,9 @@ const EpinetDurationSelector = ({
|
|
|
402
361
|
startHour: now.getHours().toString().padStart(2, '0'),
|
|
403
362
|
endHour: now.getHours().toString().padStart(2, '0'),
|
|
404
363
|
});
|
|
405
|
-
|
|
406
364
|
const endDate = new Date(now);
|
|
407
365
|
let startDate = new Date(endDate);
|
|
408
366
|
startDate.setDate(startDate.getDate() - 7);
|
|
409
|
-
|
|
410
367
|
setStartDate(startDate);
|
|
411
368
|
setEndDate(endDate);
|
|
412
369
|
setIsDatePickerOpen(false);
|
|
@@ -428,7 +385,6 @@ const EpinetDurationSelector = ({
|
|
|
428
385
|
const getFilterStatusMessage = () => {
|
|
429
386
|
const needsApply = hasLocalChanges;
|
|
430
387
|
const prefix = needsApply ? 'Press Apply Filters to load' : 'Showing';
|
|
431
|
-
|
|
432
388
|
let baseMessage: string;
|
|
433
389
|
if (needsApply && startDate && endDate) {
|
|
434
390
|
baseMessage = `${prefix} from ${formatDateHourDisplay(startDate, localFilters.startHour, false)} to ${formatDateHourDisplay(endDate, localFilters.endHour, true)}`;
|
|
@@ -441,31 +397,16 @@ const EpinetDurationSelector = ({
|
|
|
441
397
|
} else {
|
|
442
398
|
baseMessage = `${prefix} from last 7 days`;
|
|
443
399
|
}
|
|
444
|
-
|
|
445
400
|
const userInfo = needsApply
|
|
446
401
|
? localFilters.selectedUserId
|
|
447
402
|
? ` for individual user ${localFilters.selectedUserId}`
|
|
448
|
-
: ` for ${
|
|
449
|
-
localFilters.visitorType === 'all'
|
|
450
|
-
? 'all visitors'
|
|
451
|
-
: localFilters.visitorType === 'anonymous'
|
|
452
|
-
? 'anonymous visitors'
|
|
453
|
-
: 'known leads'
|
|
454
|
-
}`
|
|
403
|
+
: ` for ${localFilters.visitorType === 'all' ? 'all visitors' : localFilters.visitorType === 'anonymous' ? 'anonymous visitors' : 'known leads'}`
|
|
455
404
|
: $epinetCustomFilters.selectedUserId
|
|
456
405
|
? ` for individual user ${$epinetCustomFilters.selectedUserId}`
|
|
457
|
-
: ` for ${
|
|
458
|
-
$epinetCustomFilters.visitorType === 'all'
|
|
459
|
-
? 'all visitors'
|
|
460
|
-
: $epinetCustomFilters.visitorType === 'anonymous'
|
|
461
|
-
? 'anonymous visitors'
|
|
462
|
-
: 'known leads'
|
|
463
|
-
}`;
|
|
464
|
-
|
|
406
|
+
: ` for ${$epinetCustomFilters.visitorType === 'all' ? 'all visitors' : $epinetCustomFilters.visitorType === 'anonymous' ? 'anonymous visitors' : 'known leads'}`;
|
|
465
407
|
return baseMessage + userInfo;
|
|
466
408
|
};
|
|
467
409
|
|
|
468
|
-
// Get bar styling based on current state
|
|
469
410
|
const getBarStyling = () => {
|
|
470
411
|
if (isApplying) {
|
|
471
412
|
return {
|
|
@@ -527,16 +468,10 @@ const EpinetDurationSelector = ({
|
|
|
527
468
|
};
|
|
528
469
|
|
|
529
470
|
const styling = getBarStyling();
|
|
530
|
-
|
|
531
|
-
// Get message based on current state
|
|
532
471
|
const getMessage = () => {
|
|
533
|
-
if (isApplying)
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
return errorMessage;
|
|
537
|
-
} else {
|
|
538
|
-
return getFilterStatusMessage();
|
|
539
|
-
}
|
|
472
|
+
if (isApplying) return 'Applying filters...';
|
|
473
|
+
if (errorMessage) return errorMessage;
|
|
474
|
+
return getFilterStatusMessage();
|
|
540
475
|
};
|
|
541
476
|
|
|
542
477
|
return (
|
|
@@ -546,22 +481,69 @@ const EpinetDurationSelector = ({
|
|
|
546
481
|
<div
|
|
547
482
|
className={`space-y-4 rounded-lg border-2 border-dashed border-gray-200 bg-gray-50 p-4`}
|
|
548
483
|
>
|
|
484
|
+
<div className="space-y-4">
|
|
485
|
+
{availableFilters.map((filter) => {
|
|
486
|
+
// Determine the currently selected value for this specific filter slug. Default to 'All'.
|
|
487
|
+
const selectedValue =
|
|
488
|
+
appliedFilters.find((f) => f.beliefSlug === filter.beliefSlug)
|
|
489
|
+
?.value || 'All';
|
|
490
|
+
|
|
491
|
+
const customSortedValues = [...filter.values].sort((a, b) => {
|
|
492
|
+
if (a === 'Anonymous Traffic') return -1; // Always move 'Anonymous Traffic' to the front
|
|
493
|
+
if (b === 'Anonymous Traffic') return 1;
|
|
494
|
+
return a.localeCompare(b); // Sort the rest alphabetically
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
// Prepend 'All' to the custom-sorted list
|
|
498
|
+
const pillValues = ['All', ...customSortedValues];
|
|
499
|
+
|
|
500
|
+
return (
|
|
501
|
+
<div key={filter.beliefSlug}>
|
|
502
|
+
<label className="block text-sm font-bold capitalize text-gray-700">
|
|
503
|
+
{/* Makes "beliefSlug" -> "Belief Slug" */}
|
|
504
|
+
{filter.beliefSlug.replace(/([A-Z])/g, ' $1').trim()}
|
|
505
|
+
</label>
|
|
506
|
+
<div className="mt-2 flex flex-wrap gap-2">
|
|
507
|
+
{/* Map over the newly ordered array of pill values */}
|
|
508
|
+
{pillValues.map((value) => (
|
|
509
|
+
<button
|
|
510
|
+
key={value}
|
|
511
|
+
onClick={() =>
|
|
512
|
+
onBeliefFilterChange(filter.beliefSlug, value)
|
|
513
|
+
}
|
|
514
|
+
type="button"
|
|
515
|
+
className={`flex items-center gap-x-1.5 rounded-full px-3 py-1 text-sm font-medium transition-all duration-150 focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:ring-offset-2 ${
|
|
516
|
+
selectedValue === value
|
|
517
|
+
? 'bg-cyan-600 text-white shadow-sm'
|
|
518
|
+
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
|
519
|
+
} `}
|
|
520
|
+
>
|
|
521
|
+
{value}
|
|
522
|
+
{/* Conditionally render the 'X' icon only on the selected pill, if it's not 'All' */}
|
|
523
|
+
{selectedValue === value && value !== 'All' && (
|
|
524
|
+
<XMarkIcon
|
|
525
|
+
className="h-4 w-4 cursor-pointer stroke-2 text-white/70 hover:text-white"
|
|
526
|
+
onClick={(e) => {
|
|
527
|
+
// Stop the event from bubbling up to the parent button's onClick
|
|
528
|
+
e.stopPropagation();
|
|
529
|
+
// Reset the filter to 'All'
|
|
530
|
+
onBeliefFilterChange(filter.beliefSlug, 'All');
|
|
531
|
+
}}
|
|
532
|
+
/>
|
|
533
|
+
)}
|
|
534
|
+
</button>
|
|
535
|
+
))}
|
|
536
|
+
</div>
|
|
537
|
+
</div>
|
|
538
|
+
);
|
|
539
|
+
})}
|
|
540
|
+
</div>
|
|
549
541
|
<div
|
|
550
542
|
className="flex flex-col space-y-4 md:flex-row md:gap-4 md:space-y-0"
|
|
551
543
|
style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}
|
|
552
544
|
>
|
|
553
|
-
<style>{`
|
|
554
|
-
@media (min-width: 768px) {
|
|
555
|
-
.duration-selector-container {
|
|
556
|
-
display: grid;
|
|
557
|
-
grid-template-columns: 200px 1fr;
|
|
558
|
-
gap: 1rem;
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
`}</style>
|
|
562
|
-
|
|
545
|
+
<style>{`@media (min-width: 768px) {.duration-selector-container {display: grid; grid-template-columns: 200px 1fr; gap: 1rem;}}`}</style>
|
|
563
546
|
<div className="duration-selector-container flex flex-col space-y-4 md:space-y-0">
|
|
564
|
-
{/* Left column: Narrow visitor types stacked */}
|
|
565
547
|
<div className="space-y-2">
|
|
566
548
|
<style>{radioGroupStyles}</style>
|
|
567
549
|
<RadioGroup.Root
|
|
@@ -607,32 +589,9 @@ const EpinetDurationSelector = ({
|
|
|
607
589
|
</div>
|
|
608
590
|
</RadioGroup.Root>
|
|
609
591
|
</div>
|
|
610
|
-
|
|
611
|
-
{/* Right column: Date/time controls and user select in 2 rows */}
|
|
612
592
|
<div className="space-y-4">
|
|
613
|
-
{/* Row 1: Date Range | Start Hour | End Hour */}
|
|
614
593
|
<div className="date-time-row grid grid-cols-1 gap-4">
|
|
615
|
-
<style>{`
|
|
616
|
-
.date-time-row {
|
|
617
|
-
display: grid;
|
|
618
|
-
grid-template-columns: 1fr auto auto;
|
|
619
|
-
gap: 1rem;
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
@media (max-width: 767px) {
|
|
623
|
-
.date-time-grid {
|
|
624
|
-
grid-template-columns: 1fr !important;
|
|
625
|
-
}
|
|
626
|
-
.date-time-row {
|
|
627
|
-
grid-template-columns: 1fr auto;
|
|
628
|
-
grid-template-rows: auto auto;
|
|
629
|
-
}
|
|
630
|
-
.date-time-row > div:nth-child(3) {
|
|
631
|
-
grid-column: 2;
|
|
632
|
-
grid-row: 2;
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
`}</style>
|
|
594
|
+
<style>{`.date-time-row {display: grid; grid-template-columns: 1fr auto auto; gap: 1rem;} @media (max-width: 767px) {.date-time-grid {grid-template-columns: 1fr !important;} .date-time-row {grid-template-columns: 1fr auto; grid-template-rows: auto auto;} .date-time-row > div:nth-child(3) {grid-column: 2; grid-row: 2;}}`}</style>
|
|
636
595
|
<div className="date-time-grid space-y-1">
|
|
637
596
|
<div className="block text-sm font-bold text-gray-700">
|
|
638
597
|
Date Range (Local Time)
|
|
@@ -647,7 +606,6 @@ const EpinetDurationSelector = ({
|
|
|
647
606
|
? `${formatDateDisplay(startDate)} - ${formatDateDisplay(endDate)}`
|
|
648
607
|
: 'Select date range'}
|
|
649
608
|
</button>
|
|
650
|
-
|
|
651
609
|
{isDatePickerOpen && (
|
|
652
610
|
<div className="absolute z-10 mt-1 w-full rounded-md bg-white p-2 shadow-lg sm:w-auto">
|
|
653
611
|
<div className="mb-2 flex flex-wrap justify-between gap-2">
|
|
@@ -676,7 +634,6 @@ const EpinetDurationSelector = ({
|
|
|
676
634
|
Close
|
|
677
635
|
</button>
|
|
678
636
|
</div>
|
|
679
|
-
|
|
680
637
|
<div className="mb-2">
|
|
681
638
|
<p className="text-sm font-bold">
|
|
682
639
|
Start date: {formatDateDisplay(startDate)}
|
|
@@ -685,7 +642,6 @@ const EpinetDurationSelector = ({
|
|
|
685
642
|
End date: {formatDateDisplay(endDate)}
|
|
686
643
|
</p>
|
|
687
644
|
</div>
|
|
688
|
-
|
|
689
645
|
<div className="flex flex-col gap-4 sm:flex-row">
|
|
690
646
|
<div className="flex-1">
|
|
691
647
|
<label
|
|
@@ -742,7 +698,6 @@ const EpinetDurationSelector = ({
|
|
|
742
698
|
)}
|
|
743
699
|
</div>
|
|
744
700
|
</div>
|
|
745
|
-
|
|
746
701
|
<div className="space-y-1" style={{ minWidth: '120px' }}>
|
|
747
702
|
<div className="block text-sm font-bold text-gray-700">
|
|
748
703
|
Start Hour
|
|
@@ -762,7 +717,6 @@ const EpinetDurationSelector = ({
|
|
|
762
717
|
))}
|
|
763
718
|
</select>
|
|
764
719
|
</div>
|
|
765
|
-
|
|
766
720
|
<div className="space-y-1" style={{ minWidth: '120px' }}>
|
|
767
721
|
<div className="block text-sm font-bold text-gray-700">
|
|
768
722
|
End Hour
|
|
@@ -781,8 +735,6 @@ const EpinetDurationSelector = ({
|
|
|
781
735
|
</select>
|
|
782
736
|
</div>
|
|
783
737
|
</div>
|
|
784
|
-
|
|
785
|
-
{/* Row 2: User select (spans full width) */}
|
|
786
738
|
{paginatedUserCounts.length > 0 && (
|
|
787
739
|
<div
|
|
788
740
|
className="rounded-lg border border-gray-200 bg-white p-3 shadow-sm"
|
|
@@ -816,7 +768,6 @@ const EpinetDurationSelector = ({
|
|
|
816
768
|
</div>
|
|
817
769
|
)}
|
|
818
770
|
</div>
|
|
819
|
-
|
|
820
771
|
<div>
|
|
821
772
|
<Select.Root
|
|
822
773
|
collection={createListCollection({
|
|
@@ -905,9 +856,7 @@ const EpinetDurationSelector = ({
|
|
|
905
856
|
</div>
|
|
906
857
|
</div>
|
|
907
858
|
</div>
|
|
908
|
-
|
|
909
|
-
{/* Current status display - only show when no changes pending */}
|
|
910
|
-
{!hasLocalChanges && $epinetCustomFilters.enabled && (
|
|
859
|
+
{!hasLocalChanges && (
|
|
911
860
|
<div className="flex items-center justify-between rounded-md bg-gray-100 p-2">
|
|
912
861
|
<p className="text-sm font-bold text-gray-700">
|
|
913
862
|
{getFilterStatusMessage()}
|
|
@@ -920,7 +869,6 @@ const EpinetDurationSelector = ({
|
|
|
920
869
|
</button>
|
|
921
870
|
</div>
|
|
922
871
|
)}
|
|
923
|
-
|
|
924
872
|
{showTable && (
|
|
925
873
|
<EpinetTableView
|
|
926
874
|
fullContentMap={fullContentMap || []}
|
|
@@ -931,25 +879,15 @@ const EpinetDurationSelector = ({
|
|
|
931
879
|
</div>
|
|
932
880
|
)}
|
|
933
881
|
</div>
|
|
934
|
-
|
|
935
|
-
{/* UnsavedChangesBar-style sticky bottom bar */}
|
|
936
882
|
{isVisible && (
|
|
937
883
|
<div
|
|
938
|
-
className={`fixed bottom-0 left-0 right-0 z-50 transform pr-12 transition-all duration-300 ease-in-out ${
|
|
939
|
-
isAnimating
|
|
940
|
-
? 'translate-y-0 opacity-100'
|
|
941
|
-
: 'translate-y-full opacity-0'
|
|
942
|
-
}`}
|
|
884
|
+
className={`fixed bottom-0 left-0 right-0 z-50 transform pr-12 transition-all duration-300 ease-in-out ${isAnimating ? 'translate-y-0 opacity-100' : 'translate-y-full opacity-0'}`}
|
|
943
885
|
>
|
|
944
|
-
{/* Backdrop blur overlay */}
|
|
945
886
|
<div className="absolute inset-0 bg-black/10 backdrop-blur-sm" />
|
|
946
|
-
|
|
947
|
-
{/* Main content bar */}
|
|
948
887
|
<div className="relative mx-auto max-w-7xl px-4 py-4 sm:px-6 lg:px-8">
|
|
949
888
|
<div
|
|
950
889
|
className={`flex items-center justify-between rounded-lg border px-6 py-4 shadow-lg ${styling.bgColor} ${styling.borderColor}`}
|
|
951
890
|
>
|
|
952
|
-
{/* Icon + message */}
|
|
953
891
|
<div className="flex items-center space-x-3">
|
|
954
892
|
<div className={`flex-shrink-0 ${styling.iconColor}`}>
|
|
955
893
|
{styling.icon}
|
|
@@ -965,41 +903,22 @@ const EpinetDurationSelector = ({
|
|
|
965
903
|
)}
|
|
966
904
|
</div>
|
|
967
905
|
</div>
|
|
968
|
-
|
|
969
|
-
{/* Action buttons */}
|
|
970
906
|
<div className="flex items-center space-x-3">
|
|
971
|
-
{/* Cancel button - only show when not applying */}
|
|
972
907
|
{!isApplying && (
|
|
973
908
|
<button
|
|
974
909
|
type="button"
|
|
975
910
|
onClick={cancelChanges}
|
|
976
|
-
className={`rounded-md border px-3 py-2 text-sm font-bold shadow-sm transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-offset-2 ${
|
|
977
|
-
errorMessage
|
|
978
|
-
? 'border-red-300 bg-white text-red-800 hover:bg-red-50 focus:ring-red-500'
|
|
979
|
-
: 'border-amber-300 bg-white text-amber-800 hover:bg-amber-50 focus:ring-amber-500'
|
|
980
|
-
}`}
|
|
911
|
+
className={`rounded-md border px-3 py-2 text-sm font-bold shadow-sm transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-offset-2 ${errorMessage ? 'border-red-300 bg-white text-red-800 hover:bg-red-50 focus:ring-red-500' : 'border-amber-300 bg-white text-amber-800 hover:bg-amber-50 focus:ring-amber-500'}`}
|
|
981
912
|
>
|
|
982
913
|
Cancel
|
|
983
914
|
</button>
|
|
984
915
|
)}
|
|
985
|
-
|
|
986
|
-
{/* Apply Filters button - only show when changes exist and not already applied */}
|
|
987
916
|
{hasLocalChanges && (
|
|
988
917
|
<button
|
|
989
918
|
type="button"
|
|
990
919
|
onClick={updateDateRange}
|
|
991
920
|
disabled={!startDate || !endDate || isApplying}
|
|
992
|
-
className={`rounded-md border border-transparent px-4 py-2 text-sm font-bold text-white shadow-sm transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:cursor-not-allowed ${
|
|
993
|
-
isApplying
|
|
994
|
-
? 'bg-blue-500 opacity-75'
|
|
995
|
-
: errorMessage
|
|
996
|
-
? startDate && endDate
|
|
997
|
-
? 'bg-red-600 hover:bg-red-700 focus:ring-red-500'
|
|
998
|
-
: 'bg-red-400 opacity-50'
|
|
999
|
-
: startDate && endDate
|
|
1000
|
-
? 'bg-amber-600 hover:bg-amber-700 focus:ring-amber-500'
|
|
1001
|
-
: 'bg-amber-400 opacity-50'
|
|
1002
|
-
}`}
|
|
921
|
+
className={`rounded-md border border-transparent px-4 py-2 text-sm font-bold text-white shadow-sm transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:cursor-not-allowed ${isApplying ? 'bg-blue-500 opacity-75' : errorMessage ? (startDate && endDate ? 'bg-red-600 hover:bg-red-700 focus:ring-red-500' : 'bg-red-400 opacity-50') : startDate && endDate ? 'bg-amber-600 hover:bg-amber-700 focus:ring-amber-500' : 'bg-amber-400 opacity-50'}`}
|
|
1003
922
|
>
|
|
1004
923
|
{isApplying ? 'Applying...' : 'Apply Filters'}
|
|
1005
924
|
</button>
|