astro-tractstack 2.0.0-rc.8 → 2.0.0
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/LICENSE +8 -97
- package/README.md +7 -5
- package/bin/create-tractstack.js +35 -11
- package/dist/index.js +106 -29
- package/package.json +10 -5
- package/templates/css/frontend.css +1 -1
- package/templates/custom/minimal/CodeHook.astro +13 -12
- package/templates/custom/minimal/CustomRoutes.astro +25 -31
- package/templates/custom/with-examples/CodeHook.astro +22 -11
- package/templates/custom/with-examples/CustomRoutes.astro +4 -8
- package/templates/custom/with-examples/ProductCard.astro +29 -0
- package/templates/custom/with-examples/ProductCardWrapper.astro +43 -0
- package/templates/custom/with-examples/ProductGrid.astro +64 -0
- package/templates/custom/with-examples/pages/Collections.astro +58 -98
- package/templates/gitignore +42 -0
- package/templates/prettierignore +5 -0
- package/templates/prettierrc +19 -0
- package/templates/src/client/app.js +127 -0
- package/templates/src/client/htmx.min.js +3519 -0
- package/templates/src/client/view.js +429 -0
- package/templates/src/components/Footer.astro +4 -9
- package/templates/src/components/Header.astro +67 -60
- package/templates/src/components/Menu.tsx +188 -52
- package/templates/src/components/codehooks/BunnyVideoSetup.tsx +2 -2
- package/templates/src/components/codehooks/EpinetDurationSelector.tsx +9 -13
- package/templates/src/components/codehooks/EpinetTableView.tsx +11 -7
- package/templates/src/components/codehooks/EpinetWrapper.tsx +1 -0
- package/templates/src/components/codehooks/FeaturedArticle.astro +105 -0
- package/templates/src/components/codehooks/FeaturedArticleSetup.tsx +318 -0
- package/templates/src/components/codehooks/ListContent.astro +32 -162
- package/templates/src/components/codehooks/ListContentSetup.tsx +43 -138
- package/templates/src/components/codehooks/ProductCardSetup.tsx +152 -0
- package/templates/src/components/codehooks/ProductGridSetup.tsx +274 -0
- package/templates/src/components/codehooks/SearchWidget.tsx +453 -0
- package/templates/src/components/compositor/Node.tsx +3 -6
- package/templates/src/components/compositor/PanelVisibilityWrapper.tsx +21 -11
- package/templates/src/components/compositor/elements/BunnyVideo.tsx +21 -20
- package/templates/src/components/compositor/nodes/Pane.tsx +51 -21
- package/templates/src/components/compositor/nodes/RenderChildren.tsx +6 -1
- package/templates/src/components/compositor/nodes/Widget.tsx +16 -2
- package/templates/src/components/compositor/preview/FeaturedArticlePreview.tsx +155 -0
- package/templates/src/components/compositor/preview/PaneSnapshotGenerator.tsx +20 -1
- package/templates/src/components/edit/Header.tsx +10 -4
- package/templates/src/components/edit/PanelSwitch.tsx +11 -7
- package/templates/src/components/edit/SettingsPanel.tsx +29 -18
- package/templates/src/components/edit/ToolBar.tsx +1 -28
- package/templates/src/components/edit/ToolMode.tsx +45 -32
- package/templates/src/components/edit/pane/AddPanePanel_break.tsx +12 -2
- package/templates/src/components/edit/pane/AddPanePanel_codehook.tsx +8 -2
- package/templates/src/components/edit/pane/AddPanePanel_newAICopy_modal.tsx +1 -1
- package/templates/src/components/edit/pane/ConfigPanePanel.tsx +17 -27
- package/templates/src/components/edit/pane/PageGenSelector.tsx +16 -16
- package/templates/src/components/edit/pane/PageGenSpecial.tsx +26 -49
- package/templates/src/components/edit/pane/PageGen_preview.tsx +17 -2
- package/templates/src/components/edit/pane/PanePanel_path.tsx +2 -4
- package/templates/src/components/edit/pane/PanePanel_title.tsx +243 -76
- package/templates/src/components/edit/panels/StyleBreakPanel.tsx +17 -19
- package/templates/src/components/edit/panels/StyleCodeHookPanel.tsx +48 -37
- package/templates/src/components/edit/panels/StyleElementPanel_add.tsx +60 -55
- package/templates/src/components/edit/panels/StyleImagePanel_add.tsx +56 -50
- package/templates/src/components/edit/panels/StyleLiElementPanel_add.tsx +54 -47
- package/templates/src/components/edit/panels/StyleLinkPanel_add.tsx +54 -44
- package/templates/src/components/edit/panels/StyleLinkPanel_config.tsx +113 -138
- package/templates/src/components/edit/panels/StyleParentPanel_add.tsx +54 -40
- package/templates/src/components/edit/panels/StyleWidgetPanel.tsx +3 -3
- package/templates/src/components/edit/panels/StyleWidgetPanel_add.tsx +56 -49
- package/templates/src/components/edit/panels/StyleWidgetPanel_config.tsx +14 -5
- package/templates/src/components/edit/state/SaveModal.tsx +316 -169
- package/templates/src/components/edit/storyfragment/StoryFragmentPanel_og.tsx +1 -1
- package/templates/src/components/edit/storyfragment/StoryFragmentPanel_slug.tsx +56 -55
- package/templates/src/components/edit/widgets/BunnyWidget.tsx +538 -59
- package/templates/src/components/edit/widgets/InteractiveDisclosureWidget.tsx +656 -0
- package/templates/src/components/edit/widgets/ToggleWidget.tsx +9 -16
- package/templates/src/components/fields/ArtpackImage.tsx +4 -1
- package/templates/src/components/fields/BackgroundImage.tsx +1 -1
- package/templates/src/components/fields/BackgroundImageWrapper.tsx +127 -35
- package/templates/src/components/fields/ColorPickerCombo.tsx +66 -62
- package/templates/src/components/fields/ImageUpload.tsx +1 -1
- package/templates/src/components/fields/ViewportComboBox.tsx +59 -42
- package/templates/src/components/form/ActionBuilderBeliefSelector.tsx +117 -0
- package/templates/src/components/form/ActionBuilderField.tsx +306 -87
- package/templates/src/components/search/SearchModal.tsx +420 -0
- package/templates/src/components/search/SearchResults.tsx +367 -0
- package/templates/src/components/search/SearchWrapper.tsx +46 -0
- package/templates/src/components/storykeep/Dashboard_Advanced.tsx +1 -1
- package/templates/src/components/storykeep/Dashboard_Analytics.tsx +34 -8
- package/templates/src/components/storykeep/Dashboard_Content.tsx +6 -0
- package/templates/src/components/storykeep/StoryKeepBackdrop.astro +87 -0
- package/templates/src/components/storykeep/controls/content/BeliefForm.tsx +37 -33
- package/templates/src/components/storykeep/controls/content/MenuForm.tsx +55 -7
- package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +17 -2
- package/templates/src/components/storykeep/controls/content/StoryFragmentTable.tsx +5 -8
- package/templates/src/components/storykeep/state/FetchAnalytics.tsx +274 -228
- package/templates/src/components/storykeep/widgets/Wizard.tsx +14 -7
- package/templates/src/components/tenant/RegistrationForm.tsx +1 -1
- package/templates/src/components/widgets/ImpressionWrapper.tsx +0 -1
- package/templates/src/constants/shapes.ts +9 -0
- package/templates/src/constants.ts +2121 -16
- package/templates/src/hooks/useSearch.ts +228 -0
- package/templates/src/layouts/Layout.astro +213 -104
- package/templates/src/lib/storyData.ts +4 -1
- package/templates/src/pages/[...slug]/edit.astro +14 -14
- package/templates/src/pages/[...slug].astro +82 -21
- package/templates/src/pages/api/orphan-analysis.ts +0 -1
- package/templates/src/pages/api/tailwind.ts +23 -21
- package/templates/src/pages/context/[...contextSlug]/edit.astro +14 -14
- package/templates/src/pages/context/[...contextSlug].astro +7 -2
- package/templates/src/pages/storykeep/advanced.astro +5 -4
- package/templates/src/pages/storykeep/branding.astro +5 -4
- package/templates/src/pages/storykeep/content.astro +5 -4
- package/templates/src/pages/storykeep/init.astro +40 -1
- package/templates/src/pages/storykeep/login.astro +1 -1
- package/templates/src/pages/storykeep.astro +5 -4
- package/templates/src/stores/nodes.ts +59 -88
- package/templates/src/stores/orphanAnalysis.ts +19 -21
- package/templates/src/stores/storykeep.ts +7 -0
- package/templates/src/types/compositorTypes.ts +6 -0
- package/templates/src/types/tractstack.ts +17 -0
- package/templates/src/utils/actions/lispLexer.ts +2 -2
- package/templates/src/utils/actions/preParse_Action.ts +3 -0
- package/templates/src/utils/api/beliefHelpers.ts +12 -36
- package/templates/src/utils/api/menuHelpers.ts +2 -2
- package/templates/src/utils/api.ts +26 -0
- package/templates/src/utils/compositor/TemplateNodes.ts +7 -0
- package/templates/src/utils/compositor/allowInsert.ts +5 -3
- package/templates/src/utils/compositor/nodesHelper.ts +4 -0
- package/templates/src/utils/compositor/processMarkdown.ts +16 -2
- package/templates/src/utils/compositor/reduceNodesClassNames.ts +4 -0
- package/templates/src/utils/compositor/templateMarkdownStyles.ts +13 -13
- package/templates/src/utils/compositor/typeGuards.ts +1 -0
- package/templates/src/utils/customHelpers.ts +38 -0
- package/templates/src/utils/helpers.ts +2 -2
- package/templates/src/utils/layout.ts +65 -144
- package/utils/inject-files.ts +95 -18
- package/templates/src/client/analytics-events.js +0 -207
- package/templates/src/client/belief-events.js +0 -191
- package/templates/src/client/sse.js +0 -613
- package/templates/src/components/codehooks/FeaturedContent.astro +0 -273
- package/templates/src/components/codehooks/FeaturedContentSetup.tsx +0 -738
- package/templates/src/components/compositor/preview/FeaturedContentPreview.tsx +0 -128
- package/templates/src/components/edit/pane/PanePanel_slug.tsx +0 -219
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect,
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
2
|
import { useStore } from '@nanostores/react';
|
|
3
3
|
import { epinetCustomFilters } from '@/stores/analytics';
|
|
4
4
|
import { TractStackAPI } from '@/utils/api';
|
|
@@ -20,88 +20,115 @@ interface FetchAnalyticsProps {
|
|
|
20
20
|
onAnalyticsUpdate: (analytics: AnalyticsState) => void;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
23
|
+
// Global singleton state to prevent multi-component conflicts
|
|
24
|
+
class AnalyticsService {
|
|
25
|
+
private static instance: AnalyticsService;
|
|
26
|
+
private isInitialized = false;
|
|
27
|
+
private activeRequest: AbortController | null = null;
|
|
28
|
+
private requestCache = new Map<string, { data: any; timestamp: number }>();
|
|
29
|
+
private readonly CACHE_TTL = 5000; // 5 seconds
|
|
30
|
+
private readonly DEBOUNCE_MS = 300;
|
|
31
|
+
private debounceTimer: NodeJS.Timeout | null = null;
|
|
32
|
+
private floodProtection = {
|
|
33
|
+
requestCount: 0,
|
|
34
|
+
windowStart: 0,
|
|
35
|
+
isBlocked: false,
|
|
36
|
+
};
|
|
37
|
+
private readonly FLOOD_WINDOW_MS = 10000; // 10 seconds
|
|
38
|
+
private readonly FLOOD_THRESHOLD = 5;
|
|
39
|
+
|
|
40
|
+
static getInstance(): AnalyticsService {
|
|
41
|
+
if (!AnalyticsService.instance) {
|
|
42
|
+
AnalyticsService.instance = new AnalyticsService();
|
|
43
|
+
}
|
|
44
|
+
return AnalyticsService.instance;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private getCacheKey(params: URLSearchParams): string {
|
|
48
|
+
const sorted = new URLSearchParams([...params.entries()].sort());
|
|
49
|
+
return sorted.toString();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private isFloodBlocked(): boolean {
|
|
53
|
+
const now = Date.now();
|
|
54
|
+
|
|
55
|
+
// Reset window if needed
|
|
56
|
+
if (now - this.floodProtection.windowStart > this.FLOOD_WINDOW_MS) {
|
|
57
|
+
this.floodProtection.requestCount = 0;
|
|
58
|
+
this.floodProtection.windowStart = now;
|
|
59
|
+
this.floodProtection.isBlocked = false;
|
|
60
|
+
}
|
|
51
61
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
62
|
+
// Check if blocked
|
|
63
|
+
if (this.floodProtection.isBlocked) {
|
|
64
|
+
if (VERBOSE) console.log('🚫 Request blocked by flood protection');
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Increment counter and check threshold
|
|
69
|
+
this.floodProtection.requestCount++;
|
|
70
|
+
if (this.floodProtection.requestCount > this.FLOOD_THRESHOLD) {
|
|
71
|
+
this.floodProtection.isBlocked = true;
|
|
72
|
+
if (VERBOSE) console.log('🚨 Flood protection activated');
|
|
73
|
+
|
|
74
|
+
// Auto-unblock after delay
|
|
75
|
+
setTimeout(() => {
|
|
76
|
+
this.floodProtection.isBlocked = false;
|
|
77
|
+
if (VERBOSE) console.log('✅ Flood protection deactivated');
|
|
78
|
+
}, 30000); // 30 second cooldown
|
|
79
|
+
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private getCachedResponse(cacheKey: string): any | null {
|
|
87
|
+
const cached = this.requestCache.get(cacheKey);
|
|
88
|
+
if (cached && Date.now() - cached.timestamp < this.CACHE_TTL) {
|
|
89
|
+
if (VERBOSE) console.log('📦 Using cached response');
|
|
90
|
+
return cached.data;
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private setCachedResponse(cacheKey: string, data: any): void {
|
|
96
|
+
this.requestCache.set(cacheKey, { data, timestamp: Date.now() });
|
|
97
|
+
|
|
98
|
+
// Cleanup old cache entries
|
|
99
|
+
const cutoff = Date.now() - this.CACHE_TTL;
|
|
100
|
+
for (const [key, value] of this.requestCache.entries()) {
|
|
101
|
+
if (value.timestamp < cutoff) {
|
|
102
|
+
this.requestCache.delete(key);
|
|
57
103
|
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
visitorType: $epinetCustomFilters.visitorType,
|
|
70
|
-
selectedUserId: $epinetCustomFilters.selectedUserId,
|
|
71
|
-
},
|
|
72
|
-
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async fetchAnalytics(
|
|
108
|
+
filters: any,
|
|
109
|
+
onUpdate: (data: AnalyticsState) => void
|
|
110
|
+
): Promise<void> {
|
|
111
|
+
// Flood protection
|
|
112
|
+
if (this.isFloodBlocked()) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
73
115
|
|
|
74
116
|
try {
|
|
75
|
-
//
|
|
76
|
-
if (
|
|
77
|
-
|
|
78
|
-
|
|
117
|
+
// Cancel any existing request
|
|
118
|
+
if (this.activeRequest) {
|
|
119
|
+
this.activeRequest.abort();
|
|
120
|
+
if (VERBOSE) console.log('🛑 Cancelled previous request');
|
|
79
121
|
}
|
|
80
122
|
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
onAnalyticsUpdate({
|
|
84
|
-
dashboard: null,
|
|
85
|
-
leads: null,
|
|
86
|
-
epinet: null,
|
|
87
|
-
userCounts: [],
|
|
88
|
-
hourlyNodeActivity: {},
|
|
89
|
-
isLoading: true,
|
|
90
|
-
status: 'loading',
|
|
91
|
-
error: null,
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
const { startTimeUTC, endTimeUTC, visitorType, selectedUserId } =
|
|
95
|
-
$epinetCustomFilters;
|
|
123
|
+
// Create new abort controller
|
|
124
|
+
this.activeRequest = new AbortController();
|
|
96
125
|
|
|
97
|
-
// Build URL parameters
|
|
126
|
+
// Build URL parameters
|
|
98
127
|
const params = new URLSearchParams();
|
|
99
|
-
|
|
100
|
-
if (startTimeUTC && endTimeUTC) {
|
|
101
|
-
// Convert UTC timestamps to hours-back integers (what backend expects)
|
|
128
|
+
if (filters.startTimeUTC && filters.endTimeUTC) {
|
|
102
129
|
const now = new Date();
|
|
103
|
-
const startTime = new Date(startTimeUTC);
|
|
104
|
-
const endTime = new Date(endTimeUTC);
|
|
130
|
+
const startTime = new Date(filters.startTimeUTC);
|
|
131
|
+
const endTime = new Date(filters.endTimeUTC);
|
|
105
132
|
|
|
106
133
|
const startHour = Math.ceil(
|
|
107
134
|
(now.getTime() - startTime.getTime()) / (1000 * 60 * 60)
|
|
@@ -112,27 +139,42 @@ export default function FetchAnalytics({
|
|
|
112
139
|
|
|
113
140
|
params.append('startHour', startHour.toString());
|
|
114
141
|
params.append('endHour', endHour.toString());
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (filters.visitorType)
|
|
145
|
+
params.append('visitorType', filters.visitorType);
|
|
146
|
+
if (filters.selectedUserId)
|
|
147
|
+
params.append('userId', filters.selectedUserId);
|
|
115
148
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
});
|
|
149
|
+
const cacheKey = this.getCacheKey(params);
|
|
150
|
+
|
|
151
|
+
// Check cache first
|
|
152
|
+
const cachedData = this.getCachedResponse(cacheKey);
|
|
153
|
+
if (cachedData) {
|
|
154
|
+
onUpdate(cachedData);
|
|
155
|
+
return;
|
|
124
156
|
}
|
|
125
157
|
|
|
126
|
-
|
|
127
|
-
|
|
158
|
+
// Set loading state
|
|
159
|
+
onUpdate({
|
|
160
|
+
dashboard: null,
|
|
161
|
+
leads: null,
|
|
162
|
+
epinet: null,
|
|
163
|
+
userCounts: [],
|
|
164
|
+
hourlyNodeActivity: {},
|
|
165
|
+
isLoading: true,
|
|
166
|
+
status: 'loading',
|
|
167
|
+
error: null,
|
|
168
|
+
});
|
|
128
169
|
|
|
129
|
-
//
|
|
170
|
+
// Make request using existing TractStackAPI
|
|
130
171
|
const api = new TractStackAPI(
|
|
131
172
|
window.TRACTSTACK_CONFIG?.tenantId || 'default'
|
|
132
173
|
);
|
|
133
174
|
const endpoint = `/api/v1/analytics/all${params.toString() ? `?${params.toString()}` : ''}`;
|
|
134
175
|
|
|
135
|
-
if (VERBOSE) console.log('
|
|
176
|
+
if (VERBOSE) console.log('🔥 Making API request', { endpoint });
|
|
177
|
+
|
|
136
178
|
const response = await api.get(endpoint);
|
|
137
179
|
|
|
138
180
|
if (!response.success) {
|
|
@@ -140,15 +182,8 @@ export default function FetchAnalytics({
|
|
|
140
182
|
}
|
|
141
183
|
|
|
142
184
|
const data = response.data;
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
hasData: !!data,
|
|
146
|
-
dataKeys: data ? Object.keys(data) : [],
|
|
147
|
-
userCountsLength: data?.userCounts?.length || 0,
|
|
148
|
-
hasHourlyNodeActivity: !!data?.hourlyNodeActivity,
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
// Check if data is still loading - add polling logic here
|
|
185
|
+
|
|
186
|
+
// Check if data is still loading - implement polling logic
|
|
152
187
|
const isStillLoading =
|
|
153
188
|
data?.status === 'loading' ||
|
|
154
189
|
data?.status === 'refreshing' ||
|
|
@@ -159,63 +194,42 @@ export default function FetchAnalytics({
|
|
|
159
194
|
data?.epinet?.status === 'loading' ||
|
|
160
195
|
data?.epinet?.status === 'refreshing';
|
|
161
196
|
|
|
162
|
-
if (
|
|
163
|
-
console.log('
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const delayMs =
|
|
192
|
-
POLLING_DELAYS[pollingAttempts] ||
|
|
193
|
-
POLLING_DELAYS[POLLING_DELAYS.length - 1];
|
|
194
|
-
|
|
195
|
-
const newTimer = setTimeout(() => {
|
|
196
|
-
setPollingAttempts(pollingAttempts + 1);
|
|
197
|
-
fetchAllAnalytics();
|
|
198
|
-
}, delayMs);
|
|
199
|
-
|
|
200
|
-
setPollingTimer(newTimer);
|
|
201
|
-
|
|
202
|
-
// Update with partial data but keep loading state
|
|
203
|
-
onAnalyticsUpdate({
|
|
204
|
-
dashboard: data.dashboard,
|
|
205
|
-
leads: data.leads,
|
|
206
|
-
epinet: data.epinet,
|
|
207
|
-
userCounts: data.userCounts || [],
|
|
208
|
-
hourlyNodeActivity: data.hourlyNodeActivity || {},
|
|
209
|
-
status: 'loading',
|
|
210
|
-
error: null,
|
|
211
|
-
isLoading: true,
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
return;
|
|
197
|
+
if (isStillLoading) {
|
|
198
|
+
if (VERBOSE) console.log('⏳ Backend data still loading, will poll...');
|
|
199
|
+
|
|
200
|
+
// Update with partial data but keep loading state
|
|
201
|
+
const partialAnalytics = {
|
|
202
|
+
dashboard: data.dashboard,
|
|
203
|
+
leads: data.leads,
|
|
204
|
+
epinet: data.epinet,
|
|
205
|
+
userCounts: data.userCounts || [],
|
|
206
|
+
hourlyNodeActivity: data.hourlyNodeActivity || {},
|
|
207
|
+
status: 'loading',
|
|
208
|
+
error: null,
|
|
209
|
+
isLoading: true,
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
onUpdate(partialAnalytics);
|
|
213
|
+
|
|
214
|
+
// Schedule polling retry - use the cache key to prevent multiple polls
|
|
215
|
+
const pollKey = `poll_${cacheKey}`;
|
|
216
|
+
if (!this.requestCache.has(pollKey)) {
|
|
217
|
+
this.requestCache.set(pollKey, { data: null, timestamp: Date.now() });
|
|
218
|
+
|
|
219
|
+
setTimeout(() => {
|
|
220
|
+
this.requestCache.delete(pollKey);
|
|
221
|
+
// Clear the main cache entry to force fresh request
|
|
222
|
+
this.requestCache.delete(cacheKey);
|
|
223
|
+
// Retry the fetch
|
|
224
|
+
this.fetchAnalytics(filters, onUpdate);
|
|
225
|
+
}, 2000);
|
|
215
226
|
}
|
|
227
|
+
|
|
228
|
+
return;
|
|
216
229
|
}
|
|
217
230
|
|
|
218
|
-
|
|
231
|
+
// Process successful response
|
|
232
|
+
const analyticsData = {
|
|
219
233
|
dashboard: data.dashboard,
|
|
220
234
|
leads: data.leads,
|
|
221
235
|
epinet: data.epinet,
|
|
@@ -226,124 +240,156 @@ export default function FetchAnalytics({
|
|
|
226
240
|
isLoading: false,
|
|
227
241
|
};
|
|
228
242
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
// Update
|
|
233
|
-
|
|
234
|
-
console.log('🔄 BEFORE store update', {
|
|
235
|
-
currentStoreRef: epinetCustomFilters.get(),
|
|
236
|
-
aboutToSet: {
|
|
237
|
-
userCounts: data.userCounts?.length || 0,
|
|
238
|
-
hourlyNodeActivity: !!data.hourlyNodeActivity,
|
|
239
|
-
},
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
epinetCustomFilters.set(window.TRACTSTACK_CONFIG?.tenantId || 'default', {
|
|
243
|
-
...$epinetCustomFilters,
|
|
244
|
-
userCounts: data.userCounts || [],
|
|
245
|
-
hourlyNodeActivity: data.hourlyNodeActivity || {},
|
|
246
|
-
});
|
|
243
|
+
// Cache the response
|
|
244
|
+
this.setCachedResponse(cacheKey, analyticsData);
|
|
245
|
+
|
|
246
|
+
// Update caller
|
|
247
|
+
onUpdate(analyticsData);
|
|
247
248
|
|
|
248
|
-
|
|
249
|
-
setPollingAttempts(0);
|
|
250
|
-
setPollingStartTime(null);
|
|
249
|
+
if (VERBOSE) console.log('✅ Analytics request completed successfully');
|
|
251
250
|
|
|
252
|
-
|
|
253
|
-
console.log('🔄 AFTER store update', {
|
|
254
|
-
newStoreRef: epinetCustomFilters.get(),
|
|
255
|
-
});
|
|
251
|
+
this.activeRequest = null;
|
|
256
252
|
} catch (error) {
|
|
257
|
-
|
|
253
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
254
|
+
if (VERBOSE) console.log('🔄 Request aborted');
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
258
257
|
|
|
259
|
-
|
|
260
|
-
error instanceof Error ? error.message : 'Unknown error occurred';
|
|
258
|
+
console.error('❌ Analytics fetch error:', error);
|
|
261
259
|
|
|
262
|
-
|
|
260
|
+
onUpdate({
|
|
263
261
|
dashboard: null,
|
|
264
262
|
leads: null,
|
|
265
263
|
epinet: null,
|
|
266
264
|
userCounts: [],
|
|
267
265
|
hourlyNodeActivity: {},
|
|
268
266
|
status: 'error',
|
|
269
|
-
error:
|
|
267
|
+
error:
|
|
268
|
+
error instanceof Error ? error.message : 'Unknown error occurred',
|
|
270
269
|
isLoading: false,
|
|
271
270
|
});
|
|
272
271
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
console.log(
|
|
277
|
-
'🔄 Scheduling retry due to error, attempt',
|
|
278
|
-
pollingAttempts + 1
|
|
279
|
-
);
|
|
280
|
-
|
|
281
|
-
const delayMs =
|
|
282
|
-
POLLING_DELAYS[pollingAttempts] ||
|
|
283
|
-
POLLING_DELAYS[POLLING_DELAYS.length - 1];
|
|
284
|
-
|
|
285
|
-
const newTimer = setTimeout(() => {
|
|
286
|
-
setPollingAttempts(pollingAttempts + 1);
|
|
287
|
-
fetchAllAnalytics();
|
|
288
|
-
}, delayMs);
|
|
272
|
+
this.activeRequest = null;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
289
275
|
|
|
290
|
-
|
|
291
|
-
|
|
276
|
+
cleanup(): void {
|
|
277
|
+
if (this.activeRequest) {
|
|
278
|
+
this.activeRequest.abort();
|
|
279
|
+
this.activeRequest = null;
|
|
292
280
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
pollingAttempts,
|
|
299
|
-
]);
|
|
281
|
+
if (this.debounceTimer) {
|
|
282
|
+
clearTimeout(this.debounceTimer);
|
|
283
|
+
this.debounceTimer = null;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
300
286
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
if (!isInitialized.current && !isInitializing.current) {
|
|
304
|
-
if (VERBOSE) console.log('🏁 Initializing FetchAnalytics');
|
|
305
|
-
isInitializing.current = true;
|
|
287
|
+
initializeFilters(tenantId: string): void {
|
|
288
|
+
if (this.isInitialized) return;
|
|
306
289
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
290
|
+
if (VERBOSE) console.log('🏁 Initializing analytics filters');
|
|
291
|
+
|
|
292
|
+
const nowUTC = new Date();
|
|
293
|
+
const oneWeekAgoUTC = new Date(nowUTC.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
311
294
|
|
|
312
|
-
|
|
295
|
+
// Only set if not already initialized to prevent store churn
|
|
296
|
+
const current = epinetCustomFilters.get();
|
|
297
|
+
if (!current.enabled) {
|
|
298
|
+
epinetCustomFilters.set(tenantId, {
|
|
313
299
|
enabled: true,
|
|
314
300
|
visitorType: 'all',
|
|
315
301
|
selectedUserId: null,
|
|
316
302
|
startTimeUTC: oneWeekAgoUTC.toISOString(),
|
|
317
303
|
endTimeUTC: nowUTC.toISOString(),
|
|
318
|
-
userCounts: [],
|
|
319
|
-
hourlyNodeActivity: {},
|
|
320
304
|
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
this.isInitialized = true;
|
|
308
|
+
}
|
|
321
309
|
|
|
322
|
-
|
|
323
|
-
|
|
310
|
+
debouncedFetch(filters: any, onUpdate: (data: AnalyticsState) => void): void {
|
|
311
|
+
// Clear existing debounce timer
|
|
312
|
+
if (this.debounceTimer) {
|
|
313
|
+
clearTimeout(this.debounceTimer);
|
|
324
314
|
}
|
|
315
|
+
|
|
316
|
+
// Set new debounced fetch
|
|
317
|
+
this.debounceTimer = setTimeout(() => {
|
|
318
|
+
this.fetchAnalytics(filters, onUpdate);
|
|
319
|
+
}, this.DEBOUNCE_MS);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export default function FetchAnalytics({
|
|
324
|
+
onAnalyticsUpdate,
|
|
325
|
+
}: FetchAnalyticsProps) {
|
|
326
|
+
const $epinetCustomFilters = useStore(epinetCustomFilters);
|
|
327
|
+
const analyticsService = useRef(AnalyticsService.getInstance());
|
|
328
|
+
const lastFiltersRef = useRef<string>('');
|
|
329
|
+
|
|
330
|
+
if (VERBOSE) {
|
|
331
|
+
console.log('🔄 FetchAnalytics render', {
|
|
332
|
+
filters: {
|
|
333
|
+
startTimeUTC: $epinetCustomFilters.startTimeUTC,
|
|
334
|
+
endTimeUTC: $epinetCustomFilters.endTimeUTC,
|
|
335
|
+
visitorType: $epinetCustomFilters.visitorType,
|
|
336
|
+
selectedUserId: $epinetCustomFilters.selectedUserId,
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Cleanup on unmount
|
|
342
|
+
useEffect(() => {
|
|
343
|
+
return () => {
|
|
344
|
+
analyticsService.current.cleanup();
|
|
345
|
+
};
|
|
346
|
+
}, []);
|
|
347
|
+
|
|
348
|
+
// Initialize filters once
|
|
349
|
+
useEffect(() => {
|
|
350
|
+
const tenantId = window.TRACTSTACK_CONFIG?.tenantId || 'default';
|
|
351
|
+
analyticsService.current.initializeFilters(tenantId);
|
|
325
352
|
}, []);
|
|
326
353
|
|
|
327
|
-
//
|
|
354
|
+
// Debounced fetch when filters change
|
|
328
355
|
useEffect(() => {
|
|
329
356
|
if (
|
|
330
|
-
|
|
331
|
-
$epinetCustomFilters.
|
|
332
|
-
$epinetCustomFilters.
|
|
333
|
-
$epinetCustomFilters.
|
|
334
|
-
$epinetCustomFilters.endTimeUTC !== null
|
|
357
|
+
!$epinetCustomFilters.enabled ||
|
|
358
|
+
$epinetCustomFilters.visitorType === null ||
|
|
359
|
+
$epinetCustomFilters.startTimeUTC === null ||
|
|
360
|
+
$epinetCustomFilters.endTimeUTC === null
|
|
335
361
|
) {
|
|
336
|
-
|
|
337
|
-
setPollingAttempts(0); // Reset polling attempts when filters change
|
|
338
|
-
setPollingStartTime(null); // Reset polling start time
|
|
339
|
-
fetchAllAnalytics();
|
|
362
|
+
return;
|
|
340
363
|
}
|
|
364
|
+
|
|
365
|
+
// Create stable filter signature to prevent unnecessary fetches
|
|
366
|
+
const filtersSignature = JSON.stringify({
|
|
367
|
+
startTimeUTC: $epinetCustomFilters.startTimeUTC,
|
|
368
|
+
endTimeUTC: $epinetCustomFilters.endTimeUTC,
|
|
369
|
+
visitorType: $epinetCustomFilters.visitorType,
|
|
370
|
+
selectedUserId: $epinetCustomFilters.selectedUserId,
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// Skip if filters haven't actually changed
|
|
374
|
+
if (filtersSignature === lastFiltersRef.current) {
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
lastFiltersRef.current = filtersSignature;
|
|
379
|
+
|
|
380
|
+
if (VERBOSE) console.log('🔄 Filters changed, debouncing fetch');
|
|
381
|
+
|
|
382
|
+
analyticsService.current.debouncedFetch(
|
|
383
|
+
$epinetCustomFilters,
|
|
384
|
+
onAnalyticsUpdate
|
|
385
|
+
);
|
|
341
386
|
}, [
|
|
342
387
|
$epinetCustomFilters.enabled,
|
|
343
388
|
$epinetCustomFilters.visitorType,
|
|
344
389
|
$epinetCustomFilters.selectedUserId,
|
|
345
390
|
$epinetCustomFilters.startTimeUTC,
|
|
346
391
|
$epinetCustomFilters.endTimeUTC,
|
|
392
|
+
onAnalyticsUpdate,
|
|
347
393
|
]);
|
|
348
394
|
|
|
349
395
|
return null;
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { useState, useEffect } from 'react';
|
|
2
2
|
import { useStore } from '@nanostores/react';
|
|
3
3
|
import { skipWizard } from '@/stores/navigation';
|
|
4
|
+
import { fullContentMapStore } from '@/stores/analytics';
|
|
4
5
|
import type { FullContentMapItem, BrandConfig } from '@/types/tractstack';
|
|
5
6
|
|
|
6
7
|
interface StoryKeepWizardProps {
|
|
7
8
|
fullContentMap: FullContentMapItem[];
|
|
8
9
|
homeSlug: string;
|
|
9
|
-
brandConfig: BrandConfig;
|
|
10
|
+
brandConfig: BrandConfig;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
interface WizardData {
|
|
@@ -50,7 +51,7 @@ const wizardSteps: WizardStep[] = [
|
|
|
50
51
|
key: 'hasAnyMenu',
|
|
51
52
|
message: "A menu helps visitors navigate. Let's create one now.",
|
|
52
53
|
buttonText: 'Create a Menu',
|
|
53
|
-
href: '/storykeep/content
|
|
54
|
+
href: '/storykeep/content?create-menu',
|
|
54
55
|
},
|
|
55
56
|
{
|
|
56
57
|
key: 'hasMenu',
|
|
@@ -128,11 +129,18 @@ export default function Wizard({
|
|
|
128
129
|
const [wizardData, setWizardData] = useState<WizardData | null>(null);
|
|
129
130
|
const [loading, setLoading] = useState(true);
|
|
130
131
|
const $skipWizard = useStore(skipWizard);
|
|
132
|
+
const $clientContentMap = useStore(fullContentMapStore);
|
|
133
|
+
const activeContentMap =
|
|
134
|
+
$clientContentMap?.data?.length > 0
|
|
135
|
+
? $clientContentMap.data
|
|
136
|
+
: fullContentMap;
|
|
131
137
|
|
|
132
138
|
useEffect(() => {
|
|
133
139
|
const buildWizardData = async () => {
|
|
134
140
|
try {
|
|
135
|
-
const homePage =
|
|
141
|
+
const homePage = activeContentMap.find(
|
|
142
|
+
(item) => item.slug === homeSlug
|
|
143
|
+
);
|
|
136
144
|
|
|
137
145
|
let homeData = null;
|
|
138
146
|
if (homePage) {
|
|
@@ -165,7 +173,7 @@ export default function Wizard({
|
|
|
165
173
|
hasPanes: !!homePage?.panes?.length,
|
|
166
174
|
hasSeo: !!homePage?.description,
|
|
167
175
|
hasMenu: !!homeData?.menuId,
|
|
168
|
-
hasAnyMenu:
|
|
176
|
+
hasAnyMenu: activeContentMap.some((item) => item.type === 'Menu'),
|
|
169
177
|
};
|
|
170
178
|
|
|
171
179
|
setWizardData(data);
|
|
@@ -176,11 +184,10 @@ export default function Wizard({
|
|
|
176
184
|
}
|
|
177
185
|
};
|
|
178
186
|
|
|
179
|
-
// Only build wizard data if we have brandConfig
|
|
180
187
|
if (brandConfig) {
|
|
181
188
|
buildWizardData();
|
|
182
189
|
}
|
|
183
|
-
}, [
|
|
190
|
+
}, [activeContentMap, homeSlug, brandConfig]);
|
|
184
191
|
|
|
185
192
|
if (loading || !wizardData || !brandConfig || $skipWizard) {
|
|
186
193
|
return null;
|
|
@@ -252,7 +259,7 @@ export default function Wizard({
|
|
|
252
259
|
<div className="mt-3">
|
|
253
260
|
<a
|
|
254
261
|
href={currentStep.href}
|
|
255
|
-
className="inline-flex items-center rounded-md bg-cyan-600 px-3 py-2 text-sm font-
|
|
262
|
+
className="inline-flex items-center rounded-md bg-cyan-600 px-3 py-2 text-sm font-bold text-white shadow-sm hover:bg-cyan-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cyan-600"
|
|
256
263
|
>
|
|
257
264
|
{currentStep.buttonText}
|
|
258
265
|
<svg
|