astro-tractstack 2.0.1 → 2.0.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.
- 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 +72 -113
- package/templates/src/components/codehooks/SankeyDiagram.tsx +16 -10
- package/templates/src/components/storykeep/Dashboard_Analytics.tsx +37 -1
- package/templates/src/components/storykeep/controls/content/KnownResourceForm.tsx +62 -32
- package/templates/src/components/storykeep/state/FetchAnalytics.tsx +21 -70
- package/templates/src/stores/analytics.ts +14 -0
|
@@ -72,6 +72,24 @@ const EpinetTableView = ({
|
|
|
72
72
|
const getContentInfo = (
|
|
73
73
|
contentId: string
|
|
74
74
|
): { title: string; type: string } => {
|
|
75
|
+
if (contentId === 'commitmentAction-Previously-Entered') {
|
|
76
|
+
return { title: 'Previously Entered', type: 'Virtual' };
|
|
77
|
+
}
|
|
78
|
+
if (contentId === 'identifyAs-Anonymous-Traffic') {
|
|
79
|
+
return { title: 'Anonymous Traffic', type: 'Virtual' };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const mainParts = contentId.split('_');
|
|
83
|
+
if (mainParts[0] === 'identifyAs' && mainParts.length >= 3) {
|
|
84
|
+
const beliefSlug = mainParts[1];
|
|
85
|
+
const value = mainParts[2].replace(/-/g, ' ');
|
|
86
|
+
const titleCaseSlug = beliefSlug.replace(/([A-Z])/g, ' $1').trim();
|
|
87
|
+
return {
|
|
88
|
+
title: `${titleCaseSlug} - ${value}`,
|
|
89
|
+
type: 'Persona',
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
75
93
|
const content = fullContentMap.find((item) => item.id === contentId);
|
|
76
94
|
if (content) {
|
|
77
95
|
return {
|
|
@@ -79,8 +97,9 @@ const EpinetTableView = ({
|
|
|
79
97
|
type: content.type,
|
|
80
98
|
};
|
|
81
99
|
}
|
|
100
|
+
|
|
82
101
|
return {
|
|
83
|
-
title:
|
|
102
|
+
title: '[deleted content]',
|
|
84
103
|
type: 'Unknown',
|
|
85
104
|
};
|
|
86
105
|
};
|
|
@@ -100,11 +119,9 @@ const EpinetTableView = ({
|
|
|
100
119
|
return getHumanReadableTime(startHour);
|
|
101
120
|
}
|
|
102
121
|
const startTime = getHumanReadableTime(startHour);
|
|
103
|
-
// If end hour is 23 (11pm), show "end of day"
|
|
104
122
|
if (endHour === 23) {
|
|
105
123
|
return `${startTime} - end of day`;
|
|
106
124
|
}
|
|
107
|
-
// Show endHour:59 format
|
|
108
125
|
if (endHour === 0) {
|
|
109
126
|
return `${startTime} - 12:59am`;
|
|
110
127
|
} else if (endHour < 12) {
|
|
@@ -116,7 +133,6 @@ const EpinetTableView = ({
|
|
|
116
133
|
}
|
|
117
134
|
};
|
|
118
135
|
|
|
119
|
-
// Parse UTC hourKey and convert to local timezone for display
|
|
120
136
|
const getLocalDisplayTime = (
|
|
121
137
|
hourKey: string
|
|
122
138
|
): {
|
|
@@ -128,24 +144,20 @@ const EpinetTableView = ({
|
|
|
128
144
|
try {
|
|
129
145
|
const [year, month, day, hour] = hourKey.split('-').map(Number);
|
|
130
146
|
const utcDate = new Date(Date.UTC(year, month - 1, day, hour));
|
|
131
|
-
|
|
132
|
-
// Convert UTC to local timezone for display
|
|
133
147
|
const localDate = new Date(utcDate.toLocaleString());
|
|
134
|
-
|
|
135
148
|
const localDay = `${localDate.getFullYear()}-${String(
|
|
136
149
|
localDate.getMonth() + 1
|
|
137
150
|
).padStart(2, '0')}-${String(localDate.getDate()).padStart(2, '0')}`;
|
|
138
|
-
|
|
139
151
|
const localHour = localDate.getHours();
|
|
140
152
|
const localHourDisplay = `${localHour.toString().padStart(2, '0')}:00`;
|
|
141
153
|
const humanReadableTime = getHumanReadableTime(localHour);
|
|
142
|
-
|
|
143
154
|
return { localDay, localHour, localHourDisplay, humanReadableTime };
|
|
144
155
|
} catch (e) {
|
|
145
156
|
console.warn(`Failed to parse hourKey: ${hourKey}`, e);
|
|
146
|
-
// Fallback to treating as already local
|
|
147
157
|
const [year, month, day] = hourKey.split('-').slice(0, 3).map(Number);
|
|
148
|
-
const localDay = `${year}-${String(month).padStart(2, '0')}-${String(
|
|
158
|
+
const localDay = `${year}-${String(month).padStart(2, '0')}-${String(
|
|
159
|
+
day
|
|
160
|
+
).padStart(2, '0')}`;
|
|
149
161
|
const localHour = Number(hourKey.split('-')[3]) || 0;
|
|
150
162
|
const localHourDisplay = `${localHour.toString().padStart(2, '0')}:00`;
|
|
151
163
|
const humanReadableTime = getHumanReadableTime(localHour);
|
|
@@ -153,19 +165,15 @@ const EpinetTableView = ({
|
|
|
153
165
|
}
|
|
154
166
|
};
|
|
155
167
|
|
|
156
|
-
// Convert hourKey (UTC) to exact UTC time range for focusing
|
|
157
168
|
const focusOnThisHour = (hourKey: string) => {
|
|
158
169
|
try {
|
|
159
170
|
const [year, month, day, hour] = hourKey.split('-').map(Number);
|
|
160
|
-
|
|
161
|
-
// Create exact UTC hour boundaries
|
|
162
171
|
const startTimeUTC = new Date(
|
|
163
172
|
Date.UTC(year, month - 1, day, hour, 0, 0, 0)
|
|
164
173
|
);
|
|
165
174
|
const endTimeUTC = new Date(
|
|
166
175
|
Date.UTC(year, month - 1, day, hour, 59, 59, 999)
|
|
167
176
|
);
|
|
168
|
-
|
|
169
177
|
epinetCustomFilters.set(window.TRACTSTACK_CONFIG?.tenantId || 'default', {
|
|
170
178
|
...$epinetCustomFilters,
|
|
171
179
|
startTimeUTC: startTimeUTC.toISOString(),
|
|
@@ -173,7 +181,6 @@ const EpinetTableView = ({
|
|
|
173
181
|
});
|
|
174
182
|
} catch (e) {
|
|
175
183
|
console.warn(`Failed to focus on hour: ${hourKey}`, e);
|
|
176
|
-
// Fallback - do nothing rather than use legacy system
|
|
177
184
|
}
|
|
178
185
|
};
|
|
179
186
|
|
|
@@ -224,12 +231,11 @@ const EpinetTableView = ({
|
|
|
224
231
|
let maxHourlyTotal = 0;
|
|
225
232
|
|
|
226
233
|
const dailyUniqueVisitors = new Set<string>();
|
|
227
|
-
|
|
228
|
-
// Get current local time for "future" detection
|
|
229
234
|
const now = new Date();
|
|
230
|
-
const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(
|
|
231
|
-
|
|
232
|
-
|
|
235
|
+
const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(
|
|
236
|
+
2,
|
|
237
|
+
'0'
|
|
238
|
+
)}-${String(now.getDate()).padStart(2, '0')}`;
|
|
233
239
|
const isToday = currentDay === today;
|
|
234
240
|
const currentLocalHour = now.getHours();
|
|
235
241
|
|
|
@@ -306,7 +312,11 @@ const EpinetTableView = ({
|
|
|
306
312
|
display:
|
|
307
313
|
emptyRangeStart === localEmptyEnd
|
|
308
314
|
? `${emptyRangeStart.toString().padStart(2, '0')}:00`
|
|
309
|
-
: `${emptyRangeStart
|
|
315
|
+
: `${emptyRangeStart
|
|
316
|
+
.toString()
|
|
317
|
+
.padStart(2, '0')}:00 - ${localEmptyEnd
|
|
318
|
+
.toString()
|
|
319
|
+
.padStart(2, '0')}:59`,
|
|
310
320
|
humanReadableDisplay: getHumanReadableTimeRange(
|
|
311
321
|
emptyRangeStart,
|
|
312
322
|
localEmptyEnd
|
|
@@ -351,7 +361,9 @@ const EpinetTableView = ({
|
|
|
351
361
|
display:
|
|
352
362
|
emptyRangeStart === localEmptyEnd
|
|
353
363
|
? `${emptyRangeStart.toString().padStart(2, '0')}:00`
|
|
354
|
-
: `${emptyRangeStart.toString().padStart(2, '0')}:00 - ${localEmptyEnd
|
|
364
|
+
: `${emptyRangeStart.toString().padStart(2, '0')}:00 - ${localEmptyEnd
|
|
365
|
+
.toString()
|
|
366
|
+
.padStart(2, '0')}:59`,
|
|
355
367
|
humanReadableDisplay: getHumanReadableTimeRange(
|
|
356
368
|
emptyRangeStart,
|
|
357
369
|
localEmptyEnd
|
|
@@ -480,69 +492,82 @@ const EpinetTableView = ({
|
|
|
480
492
|
value={item.type === 'active' ? item.hourKey : `empty-${index}`}
|
|
481
493
|
className="border-b border-gray-100 last:border-b-0"
|
|
482
494
|
>
|
|
483
|
-
<Accordion.ItemTrigger className="flex w-full items-center justify-between p-3 text-left transition-colors duration-200 hover:bg-gray-
|
|
495
|
+
<Accordion.ItemTrigger className="flex w-full cursor-pointer items-center justify-between p-3 text-left transition-colors duration-200 hover:bg-gray-100">
|
|
484
496
|
{item.type === 'active' ? (
|
|
485
|
-
<div className="flex flex-grow items-center space-x-3">
|
|
486
|
-
<
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
<div
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
497
|
+
<div className="flex flex-grow items-center justify-between space-x-3">
|
|
498
|
+
<div className="flex flex-grow items-center space-x-3">
|
|
499
|
+
<span className="text-sm font-bold text-gray-700">
|
|
500
|
+
{item.humanReadableTime}
|
|
501
|
+
</span>
|
|
502
|
+
<span className="text-xs text-gray-600">
|
|
503
|
+
{item.hourlyTotal} event
|
|
504
|
+
{item.hourlyTotal !== 1 ? 's' : ''} /{' '}
|
|
505
|
+
{item.hourlyVisitors} visitor
|
|
506
|
+
{item.hourlyVisitors !== 1 ? 's' : ''}
|
|
507
|
+
</span>
|
|
508
|
+
<div className="relative h-2 w-full max-w-48 rounded bg-gray-200">
|
|
509
|
+
<div
|
|
510
|
+
className="absolute left-0 top-0 h-2 rounded bg-cyan-600"
|
|
511
|
+
style={{
|
|
512
|
+
width: `${Math.max(item.relativeToMax * 100, 5)}%`,
|
|
513
|
+
}}
|
|
514
|
+
title={`${item.hourlyTotal} events (${(
|
|
515
|
+
item.relativeToMax * 100
|
|
516
|
+
).toFixed(1)}% of busiest hour)`}
|
|
517
|
+
/>
|
|
518
|
+
</div>
|
|
503
519
|
</div>
|
|
504
|
-
<div
|
|
505
|
-
|
|
506
|
-
e
|
|
507
|
-
focusOnThisHour(item.hourKey);
|
|
508
|
-
}}
|
|
509
|
-
className="flex cursor-pointer items-center rounded-md bg-orange-100 px-2 py-1 text-xs font-bold text-orange-800 transition-colors duration-200 hover:bg-orange-200"
|
|
510
|
-
title="Focus analytics dashboard on this hour's user journeys"
|
|
511
|
-
role="button"
|
|
512
|
-
tabIndex={0}
|
|
513
|
-
onKeyDown={(e) => {
|
|
514
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
|
515
|
-
e.preventDefault();
|
|
520
|
+
<div className="flex items-center space-x-2">
|
|
521
|
+
<div
|
|
522
|
+
onClick={(e) => {
|
|
516
523
|
e.stopPropagation();
|
|
517
524
|
focusOnThisHour(item.hourKey);
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
525
|
+
}}
|
|
526
|
+
className="flex cursor-pointer items-center rounded-md bg-orange-100 px-2 py-1 text-xs font-bold text-orange-800 transition-colors duration-200 hover:bg-orange-200"
|
|
527
|
+
role="button"
|
|
528
|
+
tabIndex={0}
|
|
529
|
+
onKeyDown={(e) => {
|
|
530
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
531
|
+
e.preventDefault();
|
|
532
|
+
e.stopPropagation();
|
|
533
|
+
focusOnThisHour(item.hourKey);
|
|
534
|
+
}
|
|
535
|
+
}}
|
|
536
|
+
>
|
|
537
|
+
<MagnifyingGlassIcon className="mr-1 h-3 w-3" />
|
|
538
|
+
Journeys this Hour
|
|
539
|
+
</div>
|
|
540
|
+
<div className="flex items-center rounded-md bg-orange-100 px-2 py-1 text-xs font-bold text-orange-800">
|
|
541
|
+
<Accordion.ItemIndicator>
|
|
542
|
+
<ChevronDownIcon className="h-3 w-3 transition-transform duration-200 data-[state=open]:rotate-180" />
|
|
543
|
+
</Accordion.ItemIndicator>
|
|
544
|
+
<span className="ml-1 data-[state=closed]:block data-[state=open]:hidden">
|
|
545
|
+
Expand Details
|
|
546
|
+
</span>
|
|
547
|
+
<span className="ml-1 data-[state=open]:block data-[state=closed]:hidden">
|
|
548
|
+
Hide Details
|
|
549
|
+
</span>
|
|
550
|
+
</div>
|
|
523
551
|
</div>
|
|
524
552
|
</div>
|
|
525
553
|
) : (
|
|
526
|
-
<div className="flex flex-grow items-center">
|
|
527
|
-
<
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
554
|
+
<div className="flex flex-grow items-center justify-between">
|
|
555
|
+
<div className="flex items-center">
|
|
556
|
+
<span className="text-sm text-gray-700">
|
|
557
|
+
{item.humanReadableDisplay}
|
|
558
|
+
</span>
|
|
559
|
+
<span className="ml-2 text-xs italic text-gray-500">
|
|
560
|
+
{item.isFuture ? 'The future awaits!' : 'No activity'}
|
|
561
|
+
</span>
|
|
562
|
+
</div>
|
|
563
|
+
<Accordion.ItemIndicator>
|
|
564
|
+
<ChevronDownIcon className="h-5 w-5 text-gray-500 transition-transform duration-200 data-[state=open]:rotate-180" />
|
|
565
|
+
</Accordion.ItemIndicator>
|
|
533
566
|
</div>
|
|
534
567
|
)}
|
|
535
|
-
<Accordion.ItemIndicator>
|
|
536
|
-
<ChevronDownIcon
|
|
537
|
-
className={classNames(
|
|
538
|
-
'h-5 w-5 text-gray-500 transition-transform duration-200',
|
|
539
|
-
'data-[state=open]:rotate-180'
|
|
540
|
-
)}
|
|
541
|
-
/>
|
|
542
|
-
</Accordion.ItemIndicator>
|
|
543
568
|
</Accordion.ItemTrigger>
|
|
544
569
|
|
|
545
|
-
<Accordion.ItemContent className="
|
|
570
|
+
<Accordion.ItemContent className="p-4">
|
|
546
571
|
{item.type === 'active' && (
|
|
547
572
|
<div className="space-y-4">
|
|
548
573
|
{item.contentItems.map((content) => (
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
type ReactNode,
|
|
7
7
|
} from 'react';
|
|
8
8
|
import { useStore } from '@nanostores/react';
|
|
9
|
-
import { epinetCustomFilters } from '@/stores/analytics';
|
|
9
|
+
import { epinetCustomFilters, type AppliedFilter } from '@/stores/analytics';
|
|
10
10
|
import { TractStackAPI } from '@/utils/api';
|
|
11
11
|
import SankeyDiagram from './SankeyDiagram';
|
|
12
12
|
import EpinetDurationSelector from './EpinetDurationSelector';
|
|
@@ -34,7 +34,6 @@ const EpinetWrapper = ({
|
|
|
34
34
|
}: {
|
|
35
35
|
fullContentMap: FullContentMapItem[];
|
|
36
36
|
}) => {
|
|
37
|
-
// Use the global store instead of local state
|
|
38
37
|
const $epinetCustomFilters = useStore(epinetCustomFilters);
|
|
39
38
|
|
|
40
39
|
const [analytics, setAnalytics] = useState<{
|
|
@@ -54,15 +53,13 @@ const EpinetWrapper = ({
|
|
|
54
53
|
const [epinetId, setEpinetId] = useState<string | null>(null);
|
|
55
54
|
|
|
56
55
|
const MAX_POLLING_ATTEMPTS = 3;
|
|
57
|
-
const POLLING_DELAYS = [2000, 5000, 10000];
|
|
56
|
+
const POLLING_DELAYS = [2000, 5000, 10000];
|
|
58
57
|
|
|
59
|
-
// Initialize TractStackAPI
|
|
60
58
|
const api = useMemo(
|
|
61
59
|
() => new TractStackAPI(window.TRACTSTACK_CONFIG?.tenantId || 'default'),
|
|
62
60
|
[]
|
|
63
61
|
);
|
|
64
62
|
|
|
65
|
-
// Clear polling timer on unmount
|
|
66
63
|
useEffect(() => {
|
|
67
64
|
return () => {
|
|
68
65
|
if (pollingTimer) {
|
|
@@ -74,43 +71,32 @@ const EpinetWrapper = ({
|
|
|
74
71
|
useEffect(() => {
|
|
75
72
|
const discoverEpinetId = async () => {
|
|
76
73
|
try {
|
|
77
|
-
// First, try to find a promoted epinet from content map
|
|
78
74
|
const promotedEpinet = fullContentMap.find(
|
|
79
75
|
(item) => item.type === 'Epinet' && item.promoted
|
|
80
76
|
);
|
|
81
|
-
|
|
82
77
|
if (promotedEpinet) {
|
|
83
78
|
setEpinetId(promotedEpinet.id);
|
|
84
79
|
return;
|
|
85
80
|
}
|
|
86
|
-
|
|
87
|
-
// If no promoted epinet, get first epinet from content map
|
|
88
81
|
const firstEpinet = fullContentMap.find(
|
|
89
82
|
(item) => item.type === 'Epinet'
|
|
90
83
|
);
|
|
91
|
-
|
|
92
84
|
if (firstEpinet) {
|
|
93
85
|
setEpinetId(firstEpinet.id);
|
|
94
86
|
return;
|
|
95
87
|
}
|
|
96
|
-
|
|
97
|
-
// Fallback: no epinet found
|
|
98
|
-
console.warn('No epinet found in content map');
|
|
99
88
|
setEpinetId(null);
|
|
100
89
|
} catch (error) {
|
|
101
90
|
console.error('Error discovering epinet ID:', error);
|
|
102
91
|
setEpinetId(null);
|
|
103
92
|
}
|
|
104
93
|
};
|
|
105
|
-
|
|
106
94
|
discoverEpinetId();
|
|
107
95
|
}, [fullContentMap]);
|
|
108
96
|
|
|
109
|
-
// Initialize epinet custom filters with default values on mount
|
|
110
97
|
useEffect(() => {
|
|
111
98
|
const nowUTC = new Date();
|
|
112
99
|
const oneWeekAgoUTC = new Date(nowUTC.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
113
|
-
|
|
114
100
|
epinetCustomFilters.set(window.TRACTSTACK_CONFIG?.tenantId || 'default', {
|
|
115
101
|
enabled: true,
|
|
116
102
|
visitorType: 'all',
|
|
@@ -119,40 +105,11 @@ const EpinetWrapper = ({
|
|
|
119
105
|
endTimeUTC: nowUTC.toISOString(),
|
|
120
106
|
userCounts: [],
|
|
121
107
|
hourlyNodeActivity: {},
|
|
108
|
+
availableFilters: [],
|
|
109
|
+
appliedFilters: [],
|
|
122
110
|
});
|
|
123
111
|
}, []);
|
|
124
112
|
|
|
125
|
-
// Detect current duration type from epinetCustomFilters (for UI helpers only)
|
|
126
|
-
//const currentDurationHelper = useMemo(():
|
|
127
|
-
// | 'daily'
|
|
128
|
-
// | 'weekly'
|
|
129
|
-
// | 'monthly'
|
|
130
|
-
// | 'custom' => {
|
|
131
|
-
// const { startTimeUTC, endTimeUTC } = $epinetCustomFilters;
|
|
132
|
-
|
|
133
|
-
// if (startTimeUTC && endTimeUTC) {
|
|
134
|
-
// const startTime = new Date(startTimeUTC);
|
|
135
|
-
// const endTime = new Date(endTimeUTC);
|
|
136
|
-
// const diffMs = endTime.getTime() - startTime.getTime();
|
|
137
|
-
// const diffHours = diffMs / (1000 * 60 * 60);
|
|
138
|
-
|
|
139
|
-
// if (Math.abs(diffHours - 24) <= 1) return 'daily';
|
|
140
|
-
// if (Math.abs(diffHours - 168) <= 1) return 'weekly';
|
|
141
|
-
// if (Math.abs(diffHours - 672) <= 1) return 'monthly';
|
|
142
|
-
// return 'custom';
|
|
143
|
-
// }
|
|
144
|
-
|
|
145
|
-
// return 'weekly'; // default
|
|
146
|
-
//}, [$epinetCustomFilters.startTimeUTC, $epinetCustomFilters.endTimeUTC]);
|
|
147
|
-
|
|
148
|
-
// Fetch data when epinet ID is available
|
|
149
|
-
useEffect(() => {
|
|
150
|
-
if (epinetId) {
|
|
151
|
-
fetchEpinetData();
|
|
152
|
-
}
|
|
153
|
-
}, [epinetId]);
|
|
154
|
-
|
|
155
|
-
// Watch for changes in the global filters and refetch data
|
|
156
113
|
useEffect(() => {
|
|
157
114
|
if (
|
|
158
115
|
epinetId &&
|
|
@@ -171,43 +128,44 @@ const EpinetWrapper = ({
|
|
|
171
128
|
$epinetCustomFilters.selectedUserId,
|
|
172
129
|
$epinetCustomFilters.startTimeUTC,
|
|
173
130
|
$epinetCustomFilters.endTimeUTC,
|
|
131
|
+
$epinetCustomFilters.appliedFilters,
|
|
174
132
|
]);
|
|
175
133
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
134
|
+
const handleBeliefFilterChange = (beliefSlug: string, value: string) => {
|
|
135
|
+
const tenantId = window.TRACTSTACK_CONFIG?.tenantId || 'default';
|
|
136
|
+
const currentFilters = epinetCustomFilters.get();
|
|
137
|
+
let newFilters: AppliedFilter[] = [
|
|
138
|
+
...(currentFilters.appliedFilters || []),
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
if (value === 'All') {
|
|
142
|
+
newFilters = newFilters.filter((f) => f.beliefSlug !== beliefSlug);
|
|
143
|
+
} else {
|
|
144
|
+
const existingIndex = newFilters.findIndex(
|
|
145
|
+
(f) => f.beliefSlug === beliefSlug
|
|
146
|
+
);
|
|
147
|
+
if (existingIndex > -1) {
|
|
148
|
+
newFilters[existingIndex] = { beliefSlug, value };
|
|
149
|
+
} else {
|
|
150
|
+
newFilters.push({ beliefSlug, value });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
185
153
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
// });
|
|
192
|
-
// },
|
|
193
|
-
// [$epinetCustomFilters]
|
|
194
|
-
//);
|
|
154
|
+
epinetCustomFilters.set(tenantId, {
|
|
155
|
+
...currentFilters,
|
|
156
|
+
appliedFilters: newFilters,
|
|
157
|
+
});
|
|
158
|
+
};
|
|
195
159
|
|
|
196
160
|
const fetchEpinetData = useCallback(async () => {
|
|
197
161
|
if (!epinetId) return;
|
|
198
|
-
|
|
199
162
|
try {
|
|
200
|
-
setAnalytics((prev) => ({ ...prev, isLoading: true }));
|
|
201
|
-
|
|
163
|
+
setAnalytics((prev) => ({ ...prev, isLoading: true, status: 'loading' }));
|
|
202
164
|
if (pollingTimer) {
|
|
203
165
|
clearTimeout(pollingTimer);
|
|
204
166
|
setPollingTimer(null);
|
|
205
167
|
}
|
|
206
|
-
|
|
207
|
-
// Build query parameters
|
|
208
168
|
const params = new URLSearchParams();
|
|
209
|
-
|
|
210
|
-
// Convert UTC timestamps to hours-back integers (what backend expects)
|
|
211
169
|
if (
|
|
212
170
|
$epinetCustomFilters.startTimeUTC &&
|
|
213
171
|
$epinetCustomFilters.endTimeUTC
|
|
@@ -215,76 +173,71 @@ const EpinetWrapper = ({
|
|
|
215
173
|
const now = new Date();
|
|
216
174
|
const startTime = new Date($epinetCustomFilters.startTimeUTC);
|
|
217
175
|
const endTime = new Date($epinetCustomFilters.endTimeUTC);
|
|
218
|
-
|
|
219
176
|
const startHour = Math.ceil(
|
|
220
177
|
(now.getTime() - startTime.getTime()) / (1000 * 60 * 60)
|
|
221
178
|
);
|
|
222
179
|
const endHour = Math.floor(
|
|
223
180
|
(now.getTime() - endTime.getTime()) / (1000 * 60 * 60)
|
|
224
181
|
);
|
|
225
|
-
|
|
226
182
|
params.append('startHour', startHour.toString());
|
|
227
183
|
params.append('endHour', endHour.toString());
|
|
228
184
|
}
|
|
229
|
-
|
|
230
185
|
params.append('visitorType', $epinetCustomFilters.visitorType || 'all');
|
|
231
186
|
if ($epinetCustomFilters.selectedUserId) {
|
|
232
187
|
params.append('userId', $epinetCustomFilters.selectedUserId);
|
|
233
188
|
}
|
|
234
189
|
|
|
235
|
-
//
|
|
190
|
+
// MODIFICATION: Properly format appliedFilters for the backend
|
|
191
|
+
if (
|
|
192
|
+
$epinetCustomFilters.appliedFilters &&
|
|
193
|
+
$epinetCustomFilters.appliedFilters.length > 0
|
|
194
|
+
) {
|
|
195
|
+
params.append(
|
|
196
|
+
'appliedFilters',
|
|
197
|
+
JSON.stringify($epinetCustomFilters.appliedFilters)
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
236
201
|
const response = await api.get(
|
|
237
202
|
`/api/v1/analytics/epinet/${epinetId}?${params.toString()}`
|
|
238
203
|
);
|
|
239
|
-
|
|
240
|
-
if (!response.success) {
|
|
204
|
+
if (!response.success)
|
|
241
205
|
throw new Error(`API request failed: ${response.error}`);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
206
|
const result = response.data;
|
|
245
|
-
|
|
246
207
|
if (result.success !== false) {
|
|
247
|
-
// Check if data is still loading
|
|
248
208
|
const epinetData = result.epinet;
|
|
249
|
-
|
|
250
209
|
if (
|
|
251
210
|
epinetData &&
|
|
252
211
|
(epinetData.status === 'loading' ||
|
|
253
212
|
epinetData.status === 'refreshing')
|
|
254
213
|
) {
|
|
255
|
-
// If data is still loading, poll again after delay
|
|
256
214
|
if (pollingAttempts < MAX_POLLING_ATTEMPTS) {
|
|
257
215
|
const delayMs =
|
|
258
216
|
POLLING_DELAYS[pollingAttempts] ||
|
|
259
217
|
POLLING_DELAYS[POLLING_DELAYS.length - 1];
|
|
260
|
-
|
|
261
218
|
const newTimer = setTimeout(() => {
|
|
262
219
|
setPollingAttempts(pollingAttempts + 1);
|
|
263
220
|
fetchEpinetData();
|
|
264
221
|
}, delayMs);
|
|
265
|
-
|
|
266
222
|
setPollingTimer(newTimer);
|
|
267
223
|
return;
|
|
268
224
|
}
|
|
269
225
|
}
|
|
270
|
-
|
|
271
|
-
setAnalytics((prev) => ({
|
|
272
|
-
...prev,
|
|
226
|
+
setAnalytics({
|
|
273
227
|
epinet: result.epinet,
|
|
274
228
|
status: 'complete',
|
|
275
229
|
error: null,
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
// Update the global store with additional data from API response
|
|
230
|
+
isLoading: false,
|
|
231
|
+
});
|
|
279
232
|
epinetCustomFilters.set(
|
|
280
233
|
window.TRACTSTACK_CONFIG?.tenantId || 'default',
|
|
281
234
|
{
|
|
282
235
|
...$epinetCustomFilters,
|
|
283
236
|
userCounts: result.userCounts || [],
|
|
284
237
|
hourlyNodeActivity: result.hourlyNodeActivity || {},
|
|
238
|
+
availableFilters: result?.availableFilters || [],
|
|
285
239
|
}
|
|
286
240
|
);
|
|
287
|
-
|
|
288
241
|
setPollingAttempts(0);
|
|
289
242
|
} else {
|
|
290
243
|
throw new Error(result.error || 'Unknown API error');
|
|
@@ -295,18 +248,14 @@ const EpinetWrapper = ({
|
|
|
295
248
|
error: error instanceof Error ? error.message : 'Unknown error',
|
|
296
249
|
status: 'error',
|
|
297
250
|
}));
|
|
298
|
-
|
|
299
|
-
// Schedule a retry if we haven't reached max attempts
|
|
300
251
|
if (pollingAttempts < MAX_POLLING_ATTEMPTS) {
|
|
301
252
|
const delayMs =
|
|
302
253
|
POLLING_DELAYS[pollingAttempts] ||
|
|
303
254
|
POLLING_DELAYS[POLLING_DELAYS.length - 1];
|
|
304
|
-
|
|
305
255
|
const newTimer = setTimeout(() => {
|
|
306
256
|
setPollingAttempts(pollingAttempts + 1);
|
|
307
257
|
fetchEpinetData();
|
|
308
258
|
}, delayMs);
|
|
309
|
-
|
|
310
259
|
setPollingTimer(newTimer);
|
|
311
260
|
}
|
|
312
261
|
} finally {
|
|
@@ -316,7 +265,6 @@ const EpinetWrapper = ({
|
|
|
316
265
|
|
|
317
266
|
const { epinet, isLoading, status, error } = analytics;
|
|
318
267
|
|
|
319
|
-
// Show loading while discovering epinet ID
|
|
320
268
|
if (!epinetId) {
|
|
321
269
|
return (
|
|
322
270
|
<div className="flex h-96 w-full items-center justify-center rounded bg-gray-100">
|
|
@@ -330,18 +278,13 @@ const EpinetWrapper = ({
|
|
|
330
278
|
);
|
|
331
279
|
}
|
|
332
280
|
|
|
333
|
-
if (
|
|
334
|
-
!epinet ||
|
|
335
|
-
!epinet.nodes ||
|
|
336
|
-
!epinet.links ||
|
|
337
|
-
epinet.nodes.length === 0 ||
|
|
338
|
-
epinet.links.length === 0
|
|
339
|
-
)
|
|
281
|
+
if ((isLoading || status === 'loading') && !epinet) {
|
|
340
282
|
return (
|
|
341
283
|
<div className="flex h-64 items-center justify-center">
|
|
342
284
|
<div className="h-8 w-8 animate-spin rounded-full border-b-2 border-cyan-600"></div>
|
|
343
285
|
</div>
|
|
344
286
|
);
|
|
287
|
+
}
|
|
345
288
|
|
|
346
289
|
if (error && !epinet) {
|
|
347
290
|
return (
|
|
@@ -368,13 +311,27 @@ const EpinetWrapper = ({
|
|
|
368
311
|
epinet.nodes.length === 0 ||
|
|
369
312
|
epinet.links.length === 0
|
|
370
313
|
) {
|
|
314
|
+
if (isLoading || status === 'loading') {
|
|
315
|
+
return (
|
|
316
|
+
<div className="flex h-64 items-center justify-center">
|
|
317
|
+
<div className="h-8 w-8 animate-spin rounded-full border-b-2 border-cyan-600"></div>
|
|
318
|
+
</div>
|
|
319
|
+
);
|
|
320
|
+
}
|
|
371
321
|
return (
|
|
372
|
-
|
|
373
|
-
<p>
|
|
374
|
-
No user journey data is available
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
322
|
+
<>
|
|
323
|
+
<div className="rounded-lg bg-gray-50 p-8 text-center text-gray-800">
|
|
324
|
+
<p>No user journey data is available for the selected filters.</p>
|
|
325
|
+
</div>
|
|
326
|
+
<EpinetDurationSelector
|
|
327
|
+
fullContentMap={fullContentMap}
|
|
328
|
+
isLoading={isLoading || status === 'loading'}
|
|
329
|
+
hourlyNodeActivity={$epinetCustomFilters.hourlyNodeActivity}
|
|
330
|
+
availableFilters={$epinetCustomFilters.availableFilters}
|
|
331
|
+
appliedFilters={$epinetCustomFilters.appliedFilters}
|
|
332
|
+
onBeliefFilterChange={handleBeliefFilterChange}
|
|
333
|
+
/>
|
|
334
|
+
</>
|
|
378
335
|
);
|
|
379
336
|
}
|
|
380
337
|
|
|
@@ -396,7 +353,7 @@ const EpinetWrapper = ({
|
|
|
396
353
|
}
|
|
397
354
|
>
|
|
398
355
|
<div className="space-y-6">
|
|
399
|
-
<div className="rounded-lg bg-white p-
|
|
356
|
+
<div className="rounded-lg bg-white p-2 shadow md:p-6">
|
|
400
357
|
<div className="mb-4 flex items-center justify-between">
|
|
401
358
|
{(isLoading || status === 'loading') && (
|
|
402
359
|
<div className="flex items-center space-x-2 text-sm text-gray-500">
|
|
@@ -410,11 +367,13 @@ const EpinetWrapper = ({
|
|
|
410
367
|
isLoading={isLoading || status === 'loading'}
|
|
411
368
|
/>
|
|
412
369
|
</div>
|
|
413
|
-
|
|
414
370
|
<EpinetDurationSelector
|
|
415
371
|
fullContentMap={fullContentMap}
|
|
416
372
|
isLoading={isLoading || status === 'loading'}
|
|
417
373
|
hourlyNodeActivity={$epinetCustomFilters.hourlyNodeActivity}
|
|
374
|
+
availableFilters={$epinetCustomFilters.availableFilters}
|
|
375
|
+
appliedFilters={$epinetCustomFilters.appliedFilters}
|
|
376
|
+
onBeliefFilterChange={handleBeliefFilterChange}
|
|
418
377
|
/>
|
|
419
378
|
</div>
|
|
420
379
|
</ErrorBoundary>
|