create-analytics-demo 1.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/README.md +80 -0
- package/index.js +161 -0
- package/package.json +17 -0
- package/template/_gitignore +16 -0
- package/template/demo-server/package.json.ejs +15 -0
- package/template/demo-server/server.js.ejs +106 -0
- package/template/demo.html.ejs +28 -0
- package/template/netlify.toml +8 -0
- package/template/package.json.ejs +26 -0
- package/template/public/.gitkeep +0 -0
- package/template/remotion.config.ts +4 -0
- package/template/src/Composition.tsx +476 -0
- package/template/src/Root.tsx +18 -0
- package/template/src/components/CodePanel.tsx +130 -0
- package/template/src/components/EventParticle.tsx +60 -0
- package/template/src/components/IPhoneFrame.tsx +174 -0
- package/template/src/components/LiveCodePanel.tsx +196 -0
- package/template/src/components/ServiceLogo.tsx +106 -0
- package/template/src/components/TouchRipple.tsx +52 -0
- package/template/src/components/TravelingBall.tsx +50 -0
- package/template/src/demo/DemoApp.tsx +524 -0
- package/template/src/demo/main.tsx +12 -0
- package/template/src/demo.config.ts +137 -0
- package/template/src/screens/HomeScreen.tsx +122 -0
- package/template/src/screens/LoginScreen.tsx +94 -0
- package/template/src/screens/SplashScreen.tsx +53 -0
- package/template/start-demo.sh.ejs +27 -0
- package/template/tsconfig.json +13 -0
- package/template/vercel.json +8 -0
- package/template/vite.demo.config.ts +24 -0
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback, useRef } from "react";
|
|
2
|
+
import { config } from "../demo.config";
|
|
3
|
+
import { MiniIPhoneFrame } from "../components/IPhoneFrame";
|
|
4
|
+
import { TravelingBall, type TravelingEvent } from "../components/TravelingBall";
|
|
5
|
+
import {
|
|
6
|
+
LiveCodePanel,
|
|
7
|
+
getEventCategory,
|
|
8
|
+
shouldGoToSecondary,
|
|
9
|
+
type AnalyticsEvent,
|
|
10
|
+
} from "../components/LiveCodePanel";
|
|
11
|
+
|
|
12
|
+
const { theme } = config;
|
|
13
|
+
const [primary, secondary] = config.services;
|
|
14
|
+
|
|
15
|
+
type ConnectionStatus = "connecting" | "connected" | "disconnected";
|
|
16
|
+
|
|
17
|
+
// ── Service Logos (DOM version for live demo) ────────────────────────────────
|
|
18
|
+
|
|
19
|
+
const PrimaryLogo: React.FC<{ size?: number; isActive?: boolean }> = ({
|
|
20
|
+
size = 70,
|
|
21
|
+
isActive,
|
|
22
|
+
}) => (
|
|
23
|
+
<div
|
|
24
|
+
style={{
|
|
25
|
+
borderRadius: 20,
|
|
26
|
+
boxShadow: isActive
|
|
27
|
+
? `0 0 40px ${primary.color}`
|
|
28
|
+
: "0 10px 40px rgba(0,0,0,0.4)",
|
|
29
|
+
transition: "box-shadow 0.5s ease",
|
|
30
|
+
}}
|
|
31
|
+
>
|
|
32
|
+
{primary.logo === "svg" ? (
|
|
33
|
+
<svg width={size} height={size} viewBox="0 0 100 100" fill="none">
|
|
34
|
+
<rect width="100" height="100" rx="20" fill={primary.color} />
|
|
35
|
+
<path
|
|
36
|
+
d="M25 70 L35 45 L45 55 L55 30 L65 50 L75 25"
|
|
37
|
+
stroke="white"
|
|
38
|
+
strokeWidth="6"
|
|
39
|
+
strokeLinecap="round"
|
|
40
|
+
strokeLinejoin="round"
|
|
41
|
+
fill="none"
|
|
42
|
+
/>
|
|
43
|
+
<circle cx="75" cy="25" r="6" fill="white" />
|
|
44
|
+
</svg>
|
|
45
|
+
) : (
|
|
46
|
+
<img
|
|
47
|
+
src={`/${primary.logo}`}
|
|
48
|
+
style={{ width: size, height: size, objectFit: "contain", borderRadius: 20 }}
|
|
49
|
+
/>
|
|
50
|
+
)}
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const SecondaryLogo: React.FC<{ size?: number; isActive?: boolean }> = ({
|
|
55
|
+
size = 70,
|
|
56
|
+
isActive,
|
|
57
|
+
}) => (
|
|
58
|
+
<div
|
|
59
|
+
style={{
|
|
60
|
+
borderRadius: 20,
|
|
61
|
+
boxShadow: isActive
|
|
62
|
+
? `0 0 40px ${secondary.color}`
|
|
63
|
+
: "0 10px 40px rgba(0,0,0,0.4)",
|
|
64
|
+
transition: "box-shadow 0.5s ease",
|
|
65
|
+
}}
|
|
66
|
+
>
|
|
67
|
+
{secondary.logo === "svg" ? (
|
|
68
|
+
<svg width={size} height={size} viewBox="0 0 100 100" fill="none">
|
|
69
|
+
<rect width="100" height="100" rx="20" fill={secondary.color} />
|
|
70
|
+
<path
|
|
71
|
+
d="M25 70 L35 45 L45 55 L55 30 L65 50 L75 25"
|
|
72
|
+
stroke="white"
|
|
73
|
+
strokeWidth="6"
|
|
74
|
+
strokeLinecap="round"
|
|
75
|
+
strokeLinejoin="round"
|
|
76
|
+
fill="none"
|
|
77
|
+
/>
|
|
78
|
+
<circle cx="75" cy="25" r="6" fill="white" />
|
|
79
|
+
</svg>
|
|
80
|
+
) : (
|
|
81
|
+
<img
|
|
82
|
+
src={`/${secondary.logo}`}
|
|
83
|
+
style={{ width: size, height: size, objectFit: "contain", borderRadius: 20 }}
|
|
84
|
+
/>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
// ── Main Component ──────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
export const DemoApp: React.FC = () => {
|
|
92
|
+
const [status, setStatus] = useState<ConnectionStatus>("connecting");
|
|
93
|
+
const [eventHistory, setEventHistory] = useState<AnalyticsEvent[]>([]);
|
|
94
|
+
const [travelingEvents, setTravelingEvents] = useState<TravelingEvent[]>([]);
|
|
95
|
+
const [activeService, setActiveService] = useState({
|
|
96
|
+
primary: false,
|
|
97
|
+
secondary: false,
|
|
98
|
+
phone: false,
|
|
99
|
+
});
|
|
100
|
+
const [eventCounter, setEventCounter] = useState(0);
|
|
101
|
+
const [selectedEvent, setSelectedEvent] = useState<AnalyticsEvent | null>(null);
|
|
102
|
+
|
|
103
|
+
const primaryPathRef = useRef<SVGPathElement>(null);
|
|
104
|
+
const secondaryPathRef = useRef<SVGPathElement>(null);
|
|
105
|
+
|
|
106
|
+
// Animate traveling events at 60fps
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
const interval = setInterval(() => {
|
|
109
|
+
setTravelingEvents((current) => {
|
|
110
|
+
const updated = current.map((e) => ({
|
|
111
|
+
...e,
|
|
112
|
+
progress: Math.min(e.progress + 0.003, 1),
|
|
113
|
+
}));
|
|
114
|
+
|
|
115
|
+
updated.forEach((e) => {
|
|
116
|
+
if (e.progress >= 1) {
|
|
117
|
+
const key = e.target === primary.name ? "primary" : "secondary";
|
|
118
|
+
setActiveService((prev) => ({ ...prev, [key]: true }));
|
|
119
|
+
setTimeout(() => {
|
|
120
|
+
setActiveService((prev) => ({ ...prev, [key]: false }));
|
|
121
|
+
}, 500);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return updated.filter((e) => e.progress < 1);
|
|
126
|
+
});
|
|
127
|
+
}, 16);
|
|
128
|
+
return () => clearInterval(interval);
|
|
129
|
+
}, []);
|
|
130
|
+
|
|
131
|
+
// WebSocket connection
|
|
132
|
+
const connect = useCallback(() => {
|
|
133
|
+
setStatus("connecting");
|
|
134
|
+
const ws = new WebSocket(config.demo.wsUrl);
|
|
135
|
+
|
|
136
|
+
ws.onopen = () => setStatus("connected");
|
|
137
|
+
|
|
138
|
+
ws.onmessage = (messageEvent) => {
|
|
139
|
+
const data = JSON.parse(messageEvent.data);
|
|
140
|
+
|
|
141
|
+
if (data.type === "analytics_event") {
|
|
142
|
+
const analyticsEvent: AnalyticsEvent = {
|
|
143
|
+
event: data.event,
|
|
144
|
+
userId: data.userId,
|
|
145
|
+
deviceId: data.deviceId,
|
|
146
|
+
properties: data.properties || {},
|
|
147
|
+
timestamp: data.receivedAt,
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
setEventHistory((prev) => {
|
|
151
|
+
const isDuplicate = prev.some(
|
|
152
|
+
(e) =>
|
|
153
|
+
e.event === analyticsEvent.event &&
|
|
154
|
+
e.timestamp === analyticsEvent.timestamp
|
|
155
|
+
);
|
|
156
|
+
if (isDuplicate) return prev;
|
|
157
|
+
return [...prev, analyticsEvent];
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Create traveling events
|
|
161
|
+
setEventCounter((prev) => {
|
|
162
|
+
const newId = prev + 1;
|
|
163
|
+
const goesToSecondary = shouldGoToSecondary(analyticsEvent.event);
|
|
164
|
+
|
|
165
|
+
setTravelingEvents((cur) => {
|
|
166
|
+
const next = [...cur];
|
|
167
|
+
next.push({
|
|
168
|
+
...analyticsEvent,
|
|
169
|
+
id: newId,
|
|
170
|
+
progress: 0,
|
|
171
|
+
target: primary.name,
|
|
172
|
+
direction: "toService",
|
|
173
|
+
});
|
|
174
|
+
if (goesToSecondary) {
|
|
175
|
+
next.push({
|
|
176
|
+
...analyticsEvent,
|
|
177
|
+
id: newId + 0.5,
|
|
178
|
+
progress: 0,
|
|
179
|
+
target: secondary.name,
|
|
180
|
+
direction: "toService",
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
return next;
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
return newId;
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (data.type === "reset") {
|
|
191
|
+
setEventHistory([]);
|
|
192
|
+
setTravelingEvents([]);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
ws.onclose = () => {
|
|
197
|
+
setStatus("disconnected");
|
|
198
|
+
setTimeout(connect, 2000);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
ws.onerror = () => ws.close();
|
|
202
|
+
return ws;
|
|
203
|
+
}, []);
|
|
204
|
+
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
const ws = connect();
|
|
207
|
+
return () => ws.close();
|
|
208
|
+
}, [connect]);
|
|
209
|
+
|
|
210
|
+
const handleBallClick = (_e: React.MouseEvent, event: TravelingEvent) => {
|
|
211
|
+
setSelectedEvent(event);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const closeCodePanel = () => setSelectedEvent(null);
|
|
215
|
+
|
|
216
|
+
// Escape to close
|
|
217
|
+
useEffect(() => {
|
|
218
|
+
if (!selectedEvent) return;
|
|
219
|
+
const handler = (e: KeyboardEvent) => {
|
|
220
|
+
if (e.key === "Escape") setSelectedEvent(null);
|
|
221
|
+
};
|
|
222
|
+
document.addEventListener("keydown", handler);
|
|
223
|
+
return () => document.removeEventListener("keydown", handler);
|
|
224
|
+
}, [selectedEvent]);
|
|
225
|
+
|
|
226
|
+
return (
|
|
227
|
+
<div style={styles.container}>
|
|
228
|
+
{/* Status bar */}
|
|
229
|
+
<div style={styles.statusBar}>
|
|
230
|
+
<div style={styles.statusIndicator}>
|
|
231
|
+
<div
|
|
232
|
+
style={{
|
|
233
|
+
...styles.statusDot,
|
|
234
|
+
backgroundColor:
|
|
235
|
+
status === "connected"
|
|
236
|
+
? "#4ade80"
|
|
237
|
+
: status === "connecting"
|
|
238
|
+
? "#facc15"
|
|
239
|
+
: "#f87171",
|
|
240
|
+
}}
|
|
241
|
+
/>
|
|
242
|
+
<span style={styles.statusText}>
|
|
243
|
+
{status === "connected"
|
|
244
|
+
? "Live"
|
|
245
|
+
: status === "connecting"
|
|
246
|
+
? "Connecting..."
|
|
247
|
+
: "Disconnected"}
|
|
248
|
+
</span>
|
|
249
|
+
</div>
|
|
250
|
+
<div style={styles.eventCount}>Events: {eventHistory.length}</div>
|
|
251
|
+
</div>
|
|
252
|
+
|
|
253
|
+
{/* Main visualization */}
|
|
254
|
+
<div style={styles.mainArea}>
|
|
255
|
+
<svg
|
|
256
|
+
style={styles.svg}
|
|
257
|
+
viewBox="0 0 1000 600"
|
|
258
|
+
preserveAspectRatio="xMidYMid meet"
|
|
259
|
+
>
|
|
260
|
+
{/* Primary path */}
|
|
261
|
+
<path
|
|
262
|
+
ref={primaryPathRef}
|
|
263
|
+
d="M 200 300 Q 450 200 750 180"
|
|
264
|
+
stroke={`${primary.color}4D`}
|
|
265
|
+
strokeWidth="3"
|
|
266
|
+
fill="none"
|
|
267
|
+
strokeDasharray="8 8"
|
|
268
|
+
style={{ pointerEvents: "none" }}
|
|
269
|
+
/>
|
|
270
|
+
{/* Secondary path */}
|
|
271
|
+
<path
|
|
272
|
+
ref={secondaryPathRef}
|
|
273
|
+
d="M 200 300 Q 450 400 750 420"
|
|
274
|
+
stroke={`${secondary.color}4D`}
|
|
275
|
+
strokeWidth="3"
|
|
276
|
+
fill="none"
|
|
277
|
+
strokeDasharray="8 8"
|
|
278
|
+
style={{ pointerEvents: "none" }}
|
|
279
|
+
/>
|
|
280
|
+
|
|
281
|
+
{/* Traveling balls */}
|
|
282
|
+
{travelingEvents.map((event) => {
|
|
283
|
+
const pathRef =
|
|
284
|
+
event.target === primary.name ? primaryPathRef : secondaryPathRef;
|
|
285
|
+
return (
|
|
286
|
+
<TravelingBall
|
|
287
|
+
key={`${event.id}-${event.target}`}
|
|
288
|
+
event={event}
|
|
289
|
+
pathRef={pathRef}
|
|
290
|
+
onClick={handleBallClick}
|
|
291
|
+
color={
|
|
292
|
+
event.target === primary.name
|
|
293
|
+
? primary.color
|
|
294
|
+
: secondary.color
|
|
295
|
+
}
|
|
296
|
+
/>
|
|
297
|
+
);
|
|
298
|
+
})}
|
|
299
|
+
</svg>
|
|
300
|
+
|
|
301
|
+
{/* iPhone */}
|
|
302
|
+
<div style={styles.iphoneContainer}>
|
|
303
|
+
<div
|
|
304
|
+
style={{
|
|
305
|
+
borderRadius: 36,
|
|
306
|
+
boxShadow: activeService.phone
|
|
307
|
+
? "0 0 40px rgba(76, 175, 80, 0.8)"
|
|
308
|
+
: "none",
|
|
309
|
+
transition: "box-shadow 0.3s ease",
|
|
310
|
+
}}
|
|
311
|
+
>
|
|
312
|
+
<MiniIPhoneFrame />
|
|
313
|
+
</div>
|
|
314
|
+
<div style={styles.iphoneLabel}>{config.appName} App</div>
|
|
315
|
+
</div>
|
|
316
|
+
|
|
317
|
+
{/* Primary service */}
|
|
318
|
+
<div style={styles.primaryContainer}>
|
|
319
|
+
<PrimaryLogo size={70} isActive={activeService.primary} />
|
|
320
|
+
<div style={styles.serviceLabel}>{primary.name}</div>
|
|
321
|
+
</div>
|
|
322
|
+
|
|
323
|
+
{/* Secondary service */}
|
|
324
|
+
<div style={styles.secondaryContainer}>
|
|
325
|
+
<SecondaryLogo size={70} isActive={activeService.secondary} />
|
|
326
|
+
<div style={styles.serviceLabel}>{secondary.name}</div>
|
|
327
|
+
</div>
|
|
328
|
+
|
|
329
|
+
{travelingEvents.length > 0 && (
|
|
330
|
+
<div style={styles.hintText}>Click on a ball to see the code</div>
|
|
331
|
+
)}
|
|
332
|
+
</div>
|
|
333
|
+
|
|
334
|
+
{/* Sidebar */}
|
|
335
|
+
<div style={styles.sidebar}>
|
|
336
|
+
<h3 style={styles.sidebarTitle}>Event History</h3>
|
|
337
|
+
<div style={styles.eventList}>
|
|
338
|
+
{[...eventHistory].reverse().map((evt, i) => {
|
|
339
|
+
const goesToSec = shouldGoToSecondary(evt.event);
|
|
340
|
+
const eventColor = goesToSec ? secondary.color : primary.color;
|
|
341
|
+
|
|
342
|
+
return (
|
|
343
|
+
<div
|
|
344
|
+
key={eventHistory.length - 1 - i}
|
|
345
|
+
style={{
|
|
346
|
+
...styles.eventItem,
|
|
347
|
+
opacity: i === 0 ? 1 : 0.7,
|
|
348
|
+
borderLeftWidth: 3,
|
|
349
|
+
borderLeftColor: eventColor,
|
|
350
|
+
borderLeftStyle: "solid",
|
|
351
|
+
}}
|
|
352
|
+
onClick={() => setSelectedEvent(evt)}
|
|
353
|
+
>
|
|
354
|
+
<div style={{ ...styles.eventName, color: eventColor }}>
|
|
355
|
+
{evt.event}
|
|
356
|
+
</div>
|
|
357
|
+
<div style={styles.eventMeta}>
|
|
358
|
+
{evt.userId
|
|
359
|
+
? `User: ${evt.userId.slice(0, 8)}...`
|
|
360
|
+
: "Anonymous"}
|
|
361
|
+
</div>
|
|
362
|
+
</div>
|
|
363
|
+
);
|
|
364
|
+
})}
|
|
365
|
+
{eventHistory.length === 0 && (
|
|
366
|
+
<div style={styles.noEvents}>Waiting for events...</div>
|
|
367
|
+
)}
|
|
368
|
+
</div>
|
|
369
|
+
</div>
|
|
370
|
+
|
|
371
|
+
{/* Code Panel Overlay */}
|
|
372
|
+
{selectedEvent && (
|
|
373
|
+
<>
|
|
374
|
+
<div style={styles.overlay} onClick={closeCodePanel} />
|
|
375
|
+
<LiveCodePanel event={selectedEvent} onClose={closeCodePanel} />
|
|
376
|
+
</>
|
|
377
|
+
)}
|
|
378
|
+
|
|
379
|
+
<style>{`
|
|
380
|
+
@keyframes fadeIn {
|
|
381
|
+
from { opacity: 0; transform: translate(-50%, -50%) scale(0.95); }
|
|
382
|
+
to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
|
|
383
|
+
}
|
|
384
|
+
@keyframes pulse {
|
|
385
|
+
0%, 100% { opacity: 0.2; transform: scale(1); }
|
|
386
|
+
50% { opacity: 0.4; transform: scale(1.15); }
|
|
387
|
+
}
|
|
388
|
+
@keyframes ballGlow {
|
|
389
|
+
0%, 100% { opacity: 1; }
|
|
390
|
+
50% { opacity: 0.8; }
|
|
391
|
+
}
|
|
392
|
+
`}</style>
|
|
393
|
+
</div>
|
|
394
|
+
);
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
// ── Styles ──────────────────────────────────────────────────────────────────
|
|
398
|
+
|
|
399
|
+
const styles: { [key: string]: React.CSSProperties } = {
|
|
400
|
+
container: {
|
|
401
|
+
width: "100vw",
|
|
402
|
+
height: "100vh",
|
|
403
|
+
display: "flex",
|
|
404
|
+
flexDirection: "column",
|
|
405
|
+
background:
|
|
406
|
+
"linear-gradient(135deg, #0a0a12 0%, #12121f 50%, #0a0a12 100%)",
|
|
407
|
+
overflow: "hidden",
|
|
408
|
+
},
|
|
409
|
+
statusBar: {
|
|
410
|
+
display: "flex",
|
|
411
|
+
justifyContent: "space-between",
|
|
412
|
+
alignItems: "center",
|
|
413
|
+
padding: "12px 24px",
|
|
414
|
+
borderBottom: "1px solid rgba(255,255,255,0.1)",
|
|
415
|
+
zIndex: 10,
|
|
416
|
+
},
|
|
417
|
+
statusIndicator: { display: "flex", alignItems: "center", gap: 8 },
|
|
418
|
+
statusDot: { width: 10, height: 10, borderRadius: "50%" },
|
|
419
|
+
statusText: { color: "rgba(255,255,255,0.8)", fontSize: 14, fontWeight: 500 },
|
|
420
|
+
eventCount: { color: "rgba(255,255,255,0.6)", fontSize: 14 },
|
|
421
|
+
mainArea: { flex: 1, position: "relative", marginRight: 260 },
|
|
422
|
+
svg: { position: "absolute", top: 0, left: 0, width: "100%", height: "100%" },
|
|
423
|
+
iphoneContainer: {
|
|
424
|
+
position: "absolute",
|
|
425
|
+
left: 80,
|
|
426
|
+
top: "50%",
|
|
427
|
+
transform: "translateY(-50%)",
|
|
428
|
+
display: "flex",
|
|
429
|
+
flexDirection: "column",
|
|
430
|
+
alignItems: "center",
|
|
431
|
+
gap: 16,
|
|
432
|
+
},
|
|
433
|
+
iphoneLabel: { color: "rgba(255,255,255,0.6)", fontSize: 14, fontWeight: 500 },
|
|
434
|
+
primaryContainer: {
|
|
435
|
+
position: "absolute",
|
|
436
|
+
right: 80,
|
|
437
|
+
top: "30%",
|
|
438
|
+
transform: "translateY(-50%)",
|
|
439
|
+
display: "flex",
|
|
440
|
+
flexDirection: "column",
|
|
441
|
+
alignItems: "center",
|
|
442
|
+
gap: 12,
|
|
443
|
+
},
|
|
444
|
+
secondaryContainer: {
|
|
445
|
+
position: "absolute",
|
|
446
|
+
right: 80,
|
|
447
|
+
top: "70%",
|
|
448
|
+
transform: "translateY(-50%)",
|
|
449
|
+
display: "flex",
|
|
450
|
+
flexDirection: "column",
|
|
451
|
+
alignItems: "center",
|
|
452
|
+
gap: 12,
|
|
453
|
+
},
|
|
454
|
+
serviceLabel: { color: "white", fontSize: 16, fontWeight: 600 },
|
|
455
|
+
hintText: {
|
|
456
|
+
position: "absolute",
|
|
457
|
+
bottom: 30,
|
|
458
|
+
left: "50%",
|
|
459
|
+
transform: "translateX(-50%)",
|
|
460
|
+
color: "rgba(255,255,255,0.4)",
|
|
461
|
+
fontSize: 13,
|
|
462
|
+
fontStyle: "italic",
|
|
463
|
+
},
|
|
464
|
+
sidebar: {
|
|
465
|
+
position: "fixed",
|
|
466
|
+
right: 0,
|
|
467
|
+
top: 0,
|
|
468
|
+
bottom: 0,
|
|
469
|
+
width: 260,
|
|
470
|
+
background: "rgba(0,0,0,0.4)",
|
|
471
|
+
borderLeft: "1px solid rgba(255,255,255,0.1)",
|
|
472
|
+
padding: 20,
|
|
473
|
+
display: "flex",
|
|
474
|
+
flexDirection: "column",
|
|
475
|
+
},
|
|
476
|
+
sidebarTitle: {
|
|
477
|
+
color: "white",
|
|
478
|
+
fontSize: 16,
|
|
479
|
+
fontWeight: 600,
|
|
480
|
+
marginBottom: 16,
|
|
481
|
+
paddingBottom: 12,
|
|
482
|
+
borderBottom: "1px solid rgba(255,255,255,0.1)",
|
|
483
|
+
},
|
|
484
|
+
eventList: {
|
|
485
|
+
flex: 1,
|
|
486
|
+
overflowY: "auto",
|
|
487
|
+
display: "flex",
|
|
488
|
+
flexDirection: "column",
|
|
489
|
+
gap: 8,
|
|
490
|
+
},
|
|
491
|
+
eventItem: {
|
|
492
|
+
background: "rgba(255,255,255,0.05)",
|
|
493
|
+
borderRadius: 8,
|
|
494
|
+
padding: 12,
|
|
495
|
+
border: "1px solid rgba(255,255,255,0.1)",
|
|
496
|
+
cursor: "pointer",
|
|
497
|
+
transition: "all 0.3s ease",
|
|
498
|
+
},
|
|
499
|
+
eventName: {
|
|
500
|
+
fontSize: 13,
|
|
501
|
+
fontWeight: 600,
|
|
502
|
+
fontFamily: "SF Mono, monospace",
|
|
503
|
+
},
|
|
504
|
+
eventMeta: {
|
|
505
|
+
color: "rgba(255,255,255,0.5)",
|
|
506
|
+
fontSize: 11,
|
|
507
|
+
marginTop: 4,
|
|
508
|
+
},
|
|
509
|
+
noEvents: {
|
|
510
|
+
color: "rgba(255,255,255,0.3)",
|
|
511
|
+
fontSize: 13,
|
|
512
|
+
textAlign: "center",
|
|
513
|
+
padding: 20,
|
|
514
|
+
},
|
|
515
|
+
overlay: {
|
|
516
|
+
position: "fixed",
|
|
517
|
+
top: 0,
|
|
518
|
+
left: 0,
|
|
519
|
+
right: 0,
|
|
520
|
+
bottom: 0,
|
|
521
|
+
background: "rgba(0,0,0,0.5)",
|
|
522
|
+
zIndex: 999,
|
|
523
|
+
},
|
|
524
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import { DemoApp } from './DemoApp';
|
|
4
|
+
|
|
5
|
+
const container = document.getElementById('root');
|
|
6
|
+
const root = createRoot(container!);
|
|
7
|
+
|
|
8
|
+
root.render(
|
|
9
|
+
<React.StrictMode>
|
|
10
|
+
<DemoApp />
|
|
11
|
+
</React.StrictMode>
|
|
12
|
+
);
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// demo.config.ts — Edit this file to customize your analytics demo
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
export const config = {
|
|
6
|
+
// ── App Identity ──────────────────────────────────────────────────────────
|
|
7
|
+
appName: "MyApp",
|
|
8
|
+
|
|
9
|
+
// ── Theme ─────────────────────────────────────────────────────────────────
|
|
10
|
+
theme: {
|
|
11
|
+
bg: "#0F1119",
|
|
12
|
+
bgGradient: "#1E1E29",
|
|
13
|
+
accent: "#FC5B40",
|
|
14
|
+
accentSoft: "#FC7A57",
|
|
15
|
+
textPrimary: "#FFFFFF",
|
|
16
|
+
textSecondary: "rgba(255,255,255,0.7)",
|
|
17
|
+
surface: "rgba(255,255,255,0.08)",
|
|
18
|
+
stroke: "rgba(255,255,255,0.25)",
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
// ── Analytics Services ────────────────────────────────────────────────────
|
|
22
|
+
// The two services shown in the visualization (left = primary, right = secondary)
|
|
23
|
+
services: [
|
|
24
|
+
{
|
|
25
|
+
name: "Amplitude",
|
|
26
|
+
color: "#1E61F0",
|
|
27
|
+
// "svg" uses the built-in Amplitude logo; or set an image filename from public/
|
|
28
|
+
logo: "svg" as "svg" | string,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: "Braze",
|
|
32
|
+
color: "#A633D6",
|
|
33
|
+
// Place your logo in public/ and reference it here
|
|
34
|
+
logo: "braze.png",
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
|
|
38
|
+
// ── Event Categories ──────────────────────────────────────────────────────
|
|
39
|
+
// Maps event names (lowercase) → display category
|
|
40
|
+
eventCategories: {
|
|
41
|
+
"login completed": "Authentication",
|
|
42
|
+
"signup completed": "Authentication",
|
|
43
|
+
"logout completed": "Authentication",
|
|
44
|
+
"screen viewed": "Navigation",
|
|
45
|
+
"home viewed": "Navigation",
|
|
46
|
+
"store viewed": "Stores",
|
|
47
|
+
"product viewed": "Products",
|
|
48
|
+
"product added to cart": "Products",
|
|
49
|
+
"cart viewed": "Cart",
|
|
50
|
+
"checkout started": "Checkout",
|
|
51
|
+
"purchase completed": "Checkout",
|
|
52
|
+
"search performed": "Search",
|
|
53
|
+
"campaign opened": "Attribution",
|
|
54
|
+
"application opened": "App Lifecycle",
|
|
55
|
+
} as Record<string, string>,
|
|
56
|
+
|
|
57
|
+
// ── Secondary Service Events ──────────────────────────────────────────────
|
|
58
|
+
// Events that ALSO go to the secondary service (e.g. Braze)
|
|
59
|
+
secondaryServiceEvents: [
|
|
60
|
+
"purchase_completed",
|
|
61
|
+
"Purchase Completed",
|
|
62
|
+
"campaign_opened",
|
|
63
|
+
"Campaign Opened",
|
|
64
|
+
],
|
|
65
|
+
|
|
66
|
+
// ── Code Snippets ─────────────────────────────────────────────────────────
|
|
67
|
+
// Shown in the code panel when clicking an event ball / sidebar item
|
|
68
|
+
codeSnippets: {
|
|
69
|
+
"login completed": {
|
|
70
|
+
file: "AuthManager.swift",
|
|
71
|
+
code: [
|
|
72
|
+
"func handleLoginSuccess(user: User) {",
|
|
73
|
+
" analytics.setUserId(user.uid)",
|
|
74
|
+
" engagement.changeUser(externalId: user.uid)",
|
|
75
|
+
" ",
|
|
76
|
+
' analytics.track("Login Completed",',
|
|
77
|
+
' properties: ["method": "email"]',
|
|
78
|
+
" )",
|
|
79
|
+
"}",
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
"purchase completed": {
|
|
83
|
+
file: "CheckoutViewModel.swift",
|
|
84
|
+
code: [
|
|
85
|
+
"func completePurchase(order: Order) {",
|
|
86
|
+
' analytics.track("Purchase Completed",',
|
|
87
|
+
" properties: [",
|
|
88
|
+
' "order_id": order.id,',
|
|
89
|
+
' "total": order.total',
|
|
90
|
+
" ]",
|
|
91
|
+
" )",
|
|
92
|
+
" engagement.logPurchase(order)",
|
|
93
|
+
"}",
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
"store viewed": {
|
|
97
|
+
file: "StoreDetailViewModel.swift",
|
|
98
|
+
code: [
|
|
99
|
+
"func trackStoreView() {",
|
|
100
|
+
' analytics.track("Store Viewed",',
|
|
101
|
+
" properties: [",
|
|
102
|
+
' "store_id": store.id,',
|
|
103
|
+
' "store_name": store.name',
|
|
104
|
+
" ]",
|
|
105
|
+
" )",
|
|
106
|
+
"}",
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
} as Record<string, { file: string; code: string[] }>,
|
|
110
|
+
|
|
111
|
+
// ── App Categories (shown in the phone UI) ────────────────────────────────
|
|
112
|
+
categories: [
|
|
113
|
+
{ name: "Restaurants", emoji: "\u{1F37D}\u{FE0F}" },
|
|
114
|
+
{ name: "Groceries", emoji: "\u{1F6D2}" },
|
|
115
|
+
{ name: "Pharmacy", emoji: "\u{1F48A}" },
|
|
116
|
+
{ name: "Drinks", emoji: "\u{2615}" },
|
|
117
|
+
],
|
|
118
|
+
|
|
119
|
+
// ── Video Settings (Remotion) ─────────────────────────────────────────────
|
|
120
|
+
video: {
|
|
121
|
+
width: 1280,
|
|
122
|
+
height: 720,
|
|
123
|
+
fps: 30,
|
|
124
|
+
durationInFrames: 600,
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
// ── Server ────────────────────────────────────────────────────────────────
|
|
128
|
+
server: {
|
|
129
|
+
port: 3001,
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
// ── Demo (browser visualization) ──────────────────────────────────────────
|
|
133
|
+
demo: {
|
|
134
|
+
port: 3000,
|
|
135
|
+
wsUrl: "ws://localhost:3001",
|
|
136
|
+
},
|
|
137
|
+
};
|