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.
@@ -0,0 +1,174 @@
1
+ import React from "react";
2
+ import { config } from "../demo.config";
3
+
4
+ const { theme } = config;
5
+
6
+ /** iPhone frame wrapper for Remotion scenes */
7
+ export const IPhoneFrame: React.FC<{ children: React.ReactNode }> = ({ children }) => (
8
+ <div
9
+ style={{
10
+ width: 280,
11
+ height: 580,
12
+ borderRadius: 45,
13
+ background: "linear-gradient(145deg, #2a2a2a 0%, #1a1a1a 100%)",
14
+ padding: 8,
15
+ boxShadow:
16
+ "0 25px 80px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.1)",
17
+ position: "relative",
18
+ }}
19
+ >
20
+ {/* Notch */}
21
+ <div
22
+ style={{
23
+ position: "absolute",
24
+ top: 18,
25
+ left: "50%",
26
+ transform: "translateX(-50%)",
27
+ width: 90,
28
+ height: 26,
29
+ background: "#000",
30
+ borderRadius: 20,
31
+ zIndex: 100,
32
+ }}
33
+ />
34
+ {/* Screen */}
35
+ <div
36
+ style={{
37
+ width: "100%",
38
+ height: "100%",
39
+ borderRadius: 38,
40
+ overflow: "hidden",
41
+ background: `linear-gradient(160deg, ${theme.bg} 0%, ${theme.bgGradient} 50%, #0a0a12 100%)`,
42
+ position: "relative",
43
+ }}
44
+ >
45
+ {children}
46
+ </div>
47
+ </div>
48
+ );
49
+
50
+ /** Smaller phone frame for the live demo visualization */
51
+ export const MiniIPhoneFrame: React.FC = () => (
52
+ <div
53
+ style={{
54
+ width: 200,
55
+ height: 420,
56
+ borderRadius: 32,
57
+ background: "linear-gradient(145deg, #2a2a2a 0%, #1a1a1a 100%)",
58
+ padding: 5,
59
+ boxShadow:
60
+ "0 25px 80px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.1)",
61
+ position: "relative",
62
+ }}
63
+ >
64
+ <div
65
+ style={{
66
+ position: "absolute",
67
+ top: 12,
68
+ left: "50%",
69
+ transform: "translateX(-50%)",
70
+ width: 60,
71
+ height: 20,
72
+ background: "#000",
73
+ borderRadius: 16,
74
+ zIndex: 100,
75
+ }}
76
+ />
77
+ <div
78
+ style={{
79
+ width: "100%",
80
+ height: "100%",
81
+ borderRadius: 28,
82
+ overflow: "hidden",
83
+ background: `linear-gradient(160deg, ${theme.bg} 0%, ${theme.bgGradient} 100%)`,
84
+ position: "relative",
85
+ }}
86
+ >
87
+ <div style={{ padding: "36px 8px 8px", height: "100%" }}>
88
+ <div
89
+ style={{
90
+ fontSize: 7,
91
+ color: theme.textSecondary,
92
+ marginBottom: 2,
93
+ }}
94
+ >
95
+ DELIVERING TO
96
+ </div>
97
+ <div
98
+ style={{
99
+ fontSize: 9,
100
+ color: theme.textPrimary,
101
+ fontWeight: "600",
102
+ marginBottom: 8,
103
+ }}
104
+ >
105
+ 123 Main Street
106
+ </div>
107
+ <div
108
+ style={{
109
+ background: theme.surface,
110
+ borderRadius: 6,
111
+ padding: "5px 6px",
112
+ marginBottom: 8,
113
+ border: `1px solid ${theme.stroke}`,
114
+ }}
115
+ >
116
+ <span style={{ color: theme.textSecondary, fontSize: 8 }}>
117
+ Search...
118
+ </span>
119
+ </div>
120
+ <div
121
+ style={{
122
+ display: "grid",
123
+ gridTemplateColumns: "repeat(4, 1fr)",
124
+ gap: 3,
125
+ marginBottom: 8,
126
+ }}
127
+ >
128
+ {config.categories.map((cat, i) => (
129
+ <div
130
+ key={i}
131
+ style={{
132
+ background: theme.surface,
133
+ borderRadius: 5,
134
+ padding: 3,
135
+ textAlign: "center",
136
+ }}
137
+ >
138
+ <span style={{ fontSize: 10 }}>{cat.emoji}</span>
139
+ </div>
140
+ ))}
141
+ </div>
142
+ <div style={{ background: theme.surface, borderRadius: 8, overflow: "hidden" }}>
143
+ <div
144
+ style={{
145
+ background: "linear-gradient(135deg, #2a2a3e, #1a1a2e)",
146
+ height: 40,
147
+ display: "flex",
148
+ alignItems: "center",
149
+ justifyContent: "center",
150
+ }}
151
+ >
152
+ <span style={{ fontSize: 16 }}>
153
+ {config.categories[0]?.emoji || ""}
154
+ </span>
155
+ </div>
156
+ <div style={{ padding: 5 }}>
157
+ <div
158
+ style={{
159
+ fontSize: 8,
160
+ color: theme.textPrimary,
161
+ fontWeight: "600",
162
+ }}
163
+ >
164
+ Featured Store
165
+ </div>
166
+ <div style={{ fontSize: 6, color: theme.textSecondary }}>
167
+ 4.8 - 20-30 min
168
+ </div>
169
+ </div>
170
+ </div>
171
+ </div>
172
+ </div>
173
+ </div>
174
+ );
@@ -0,0 +1,196 @@
1
+ import React from "react";
2
+ import { config } from "../demo.config";
3
+
4
+ const { theme } = config;
5
+
6
+ interface AnalyticsEvent {
7
+ event: string;
8
+ userId: string | null;
9
+ deviceId: string;
10
+ properties: Record<string, any>;
11
+ timestamp: string;
12
+ }
13
+
14
+ /** Code panel overlay shown in the live demo when clicking an event */
15
+ export const LiveCodePanel: React.FC<{
16
+ event: AnalyticsEvent;
17
+ onClose: () => void;
18
+ }> = ({ event, onClose }) => {
19
+ const category = getEventCategory(event.event);
20
+ const goesToSecondary = shouldGoToSecondary(event.event);
21
+ const snippet = config.codeSnippets[event.event.toLowerCase()];
22
+ const [primary, secondary] = config.services;
23
+
24
+ return (
25
+ <div
26
+ style={{
27
+ position: "fixed",
28
+ left: "50%",
29
+ top: "50%",
30
+ transform: "translate(-50%, -50%)",
31
+ background: "#1e1e2e",
32
+ borderRadius: 14,
33
+ padding: 20,
34
+ border: "1px solid #444",
35
+ boxShadow: "0 25px 80px rgba(0,0,0,0.8)",
36
+ minWidth: 420,
37
+ maxWidth: 520,
38
+ zIndex: 1000,
39
+ animation: "fadeIn 0.2s ease",
40
+ }}
41
+ onClick={(e) => e.stopPropagation()}
42
+ >
43
+ {/* Header */}
44
+ <div
45
+ style={{
46
+ display: "flex",
47
+ justifyContent: "space-between",
48
+ alignItems: "center",
49
+ marginBottom: 12,
50
+ paddingBottom: 10,
51
+ borderBottom: "1px solid #333",
52
+ }}
53
+ >
54
+ <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
55
+ <div style={{ display: "flex", gap: 6 }}>
56
+ <div style={{ width: 10, height: 10, borderRadius: 5, background: "#FF5F56" }} />
57
+ <div style={{ width: 10, height: 10, borderRadius: 5, background: "#FFBD2E" }} />
58
+ <div style={{ width: 10, height: 10, borderRadius: 5, background: "#27CA40" }} />
59
+ </div>
60
+ <span style={{ color: "#666", fontSize: 11, fontFamily: "monospace", marginLeft: 8 }}>
61
+ {snippet?.file || "AnalyticsClient.swift"}
62
+ </span>
63
+ </div>
64
+ <button
65
+ onClick={onClose}
66
+ style={{ background: "none", border: "none", color: "#666", cursor: "pointer", fontSize: 20, padding: "0 8px", lineHeight: 1 }}
67
+ >
68
+ x
69
+ </button>
70
+ </div>
71
+
72
+ {/* Badges */}
73
+ <div style={{ display: "flex", gap: 8, marginBottom: 14, flexWrap: "wrap" }}>
74
+ <span
75
+ style={{
76
+ background: `rgba(252, 91, 64, 0.2)`,
77
+ color: theme.accent,
78
+ padding: "4px 10px",
79
+ borderRadius: 12,
80
+ fontSize: 10,
81
+ fontWeight: 600,
82
+ }}
83
+ >
84
+ {category}
85
+ </span>
86
+ <span
87
+ style={{
88
+ background: `${primary.color}33`,
89
+ color: primary.color,
90
+ padding: "4px 10px",
91
+ borderRadius: 12,
92
+ fontSize: 10,
93
+ fontWeight: 600,
94
+ }}
95
+ >
96
+ &rarr; {primary.name}
97
+ </span>
98
+ {goesToSecondary && (
99
+ <span
100
+ style={{
101
+ background: `${secondary.color}33`,
102
+ color: secondary.color,
103
+ padding: "4px 10px",
104
+ borderRadius: 12,
105
+ fontSize: 10,
106
+ fontWeight: 600,
107
+ }}
108
+ >
109
+ &rarr; {secondary.name}
110
+ </span>
111
+ )}
112
+ </div>
113
+
114
+ {/* Code */}
115
+ <div
116
+ style={{
117
+ fontFamily: "SF Mono, Menlo, monospace",
118
+ fontSize: 12,
119
+ lineHeight: 1.7,
120
+ background: "#16161e",
121
+ padding: 14,
122
+ borderRadius: 8,
123
+ maxHeight: 300,
124
+ overflowY: "auto",
125
+ }}
126
+ >
127
+ {snippet ? (
128
+ snippet.code.map((line, i) => {
129
+ let color = "#ABB2BF";
130
+ if (line.trim().startsWith("//")) color = "#5C6370";
131
+ else if (line.includes("func ") || line.includes("var ") || line.includes("let ")) color = "#C678DD";
132
+ else if (line.includes(".track(") || line.includes(".setUserId") || line.includes(".changeUser")) color = "#61AFEF";
133
+ return <div key={i} style={{ color, whiteSpace: "pre" }}>{line}</div>;
134
+ })
135
+ ) : (
136
+ <>
137
+ <div style={{ color: "#ABB2BF" }}>
138
+ <span style={{ color: "#61AFEF" }}>analytics</span>.
139
+ <span style={{ color: "#E5C07B" }}>track</span>(
140
+ </div>
141
+ <div style={{ color: "#ABB2BF" }}>
142
+ {" "}event: <span style={{ color: "#98C379" }}>"{event.event}"</span>,
143
+ </div>
144
+ <div style={{ color: "#ABB2BF" }}>{" "}properties: [</div>
145
+ {Object.entries(event.properties || {}).map(([key, value], i) => (
146
+ <div key={i} style={{ color: "#ABB2BF" }}>
147
+ {" "}
148
+ <span style={{ color: "#98C379" }}>"{key}"</span>:{" "}
149
+ <span style={{ color: "#D19A66" }}>
150
+ {typeof value === "string"
151
+ ? `"${value.slice(0, 30)}${value.length > 30 ? "..." : ""}"`
152
+ : JSON.stringify(value)}
153
+ </span>
154
+ ,
155
+ </div>
156
+ ))}
157
+ <div style={{ color: "#ABB2BF" }}>{" "}]</div>
158
+ <div style={{ color: "#ABB2BF" }}>)</div>
159
+ </>
160
+ )}
161
+ </div>
162
+
163
+ {/* User info */}
164
+ <div
165
+ style={{
166
+ marginTop: 12,
167
+ paddingTop: 10,
168
+ borderTop: "1px solid #333",
169
+ color: "#5C6370",
170
+ fontSize: 11,
171
+ fontFamily: "SF Mono, monospace",
172
+ }}
173
+ >
174
+ {event.userId
175
+ ? `userId: "${event.userId}"`
176
+ : `deviceId: "${event.deviceId || "unknown"}"`}
177
+ </div>
178
+ </div>
179
+ );
180
+ };
181
+
182
+ // ── Helpers ─────────────────────────────────────────────────────────────────
183
+
184
+ function getEventCategory(eventName: string): string {
185
+ return config.eventCategories[eventName.toLowerCase()] || "Analytics";
186
+ }
187
+
188
+ function shouldGoToSecondary(eventName: string): boolean {
189
+ const normalized = eventName.toLowerCase().replace(/[_\s]/g, "");
190
+ return config.secondaryServiceEvents.some(
191
+ (e) => normalized.includes(e.toLowerCase().replace(/[_\s]/g, ""))
192
+ );
193
+ }
194
+
195
+ export { getEventCategory, shouldGoToSecondary };
196
+ export type { AnalyticsEvent };
@@ -0,0 +1,106 @@
1
+ import React from "react";
2
+ import {
3
+ useCurrentFrame,
4
+ useVideoConfig,
5
+ spring,
6
+ interpolate,
7
+ Img,
8
+ staticFile,
9
+ } from "remotion";
10
+ import { config } from "../demo.config";
11
+
12
+ /** Built-in Amplitude-style SVG logo */
13
+ const AmplitudeSvg: React.FC<{ size: number; color: string }> = ({
14
+ size,
15
+ color,
16
+ }) => (
17
+ <svg width={size} height={size} viewBox="0 0 100 100" fill="none">
18
+ <rect width="100" height="100" rx="20" fill={color} />
19
+ <path
20
+ d="M25 70 L35 45 L45 55 L55 30 L65 50 L75 25"
21
+ stroke="white"
22
+ strokeWidth="6"
23
+ strokeLinecap="round"
24
+ strokeLinejoin="round"
25
+ fill="none"
26
+ />
27
+ <circle cx="75" cy="25" r="6" fill="white" />
28
+ </svg>
29
+ );
30
+
31
+ /** Renders the logo for a service (SVG built-in or image from public/) */
32
+ export const ServiceLogoImg: React.FC<{
33
+ service: (typeof config.services)[number];
34
+ size?: number;
35
+ }> = ({ service, size = 60 }) => {
36
+ if (service.logo === "svg") {
37
+ return <AmplitudeSvg size={size} color={service.color} />;
38
+ }
39
+ return (
40
+ <Img
41
+ src={staticFile(service.logo)}
42
+ style={{ width: size, height: size, objectFit: "contain" }}
43
+ />
44
+ );
45
+ };
46
+
47
+ /** Service logo card used in Remotion scenes (with spring animation) */
48
+ export const ServiceLogoCard: React.FC<{
49
+ service: (typeof config.services)[number];
50
+ x: number;
51
+ y: number;
52
+ delay: number;
53
+ isActive?: boolean;
54
+ }> = ({ service, x, y, delay, isActive = false }) => {
55
+ const frame = useCurrentFrame();
56
+ const { fps } = useVideoConfig();
57
+ const appear = spring({
58
+ frame: frame - delay,
59
+ fps,
60
+ config: { damping: 40, stiffness: 100 },
61
+ });
62
+ const pulse = isActive ? 1 + Math.sin(frame * 0.3) * 0.05 : 1;
63
+
64
+ return (
65
+ <div
66
+ style={{
67
+ position: "absolute",
68
+ left: x,
69
+ top: y,
70
+ transform: `scale(${Math.max(0, appear) * pulse})`,
71
+ opacity: Math.max(
72
+ 0,
73
+ interpolate(frame - delay, [0, 15], [0, 1], {
74
+ extrapolateLeft: "clamp",
75
+ extrapolateRight: "clamp",
76
+ })
77
+ ),
78
+ display: "flex",
79
+ flexDirection: "column",
80
+ alignItems: "center",
81
+ gap: 10,
82
+ }}
83
+ >
84
+ <div
85
+ style={{
86
+ borderRadius: 20,
87
+ boxShadow: isActive
88
+ ? `0 0 40px ${service.color}`
89
+ : "0 10px 40px rgba(0,0,0,0.4)",
90
+ }}
91
+ >
92
+ <ServiceLogoImg service={service} size={70} />
93
+ </div>
94
+ <div
95
+ style={{
96
+ color: "white",
97
+ fontSize: 16,
98
+ fontWeight: "600",
99
+ fontFamily: "system-ui",
100
+ }}
101
+ >
102
+ {service.name}
103
+ </div>
104
+ </div>
105
+ );
106
+ };
@@ -0,0 +1,52 @@
1
+ import React from "react";
2
+ import { interpolate } from "remotion";
3
+
4
+ export const TouchRipple: React.FC<{
5
+ x: number;
6
+ y: number;
7
+ frame: number;
8
+ startFrame: number;
9
+ }> = ({ x, y, frame, startFrame }) => {
10
+ const localFrame = frame - startFrame;
11
+ if (localFrame < 0 || localFrame > 30) return null;
12
+
13
+ const scale1 = interpolate(localFrame, [0, 20], [0, 2], {
14
+ extrapolateRight: "clamp",
15
+ });
16
+ const opacity1 = interpolate(localFrame, [0, 20], [0.6, 0], {
17
+ extrapolateRight: "clamp",
18
+ });
19
+ const dotScale = interpolate(localFrame, [0, 5, 15], [0, 1, 0], {
20
+ extrapolateRight: "clamp",
21
+ });
22
+
23
+ return (
24
+ <>
25
+ <div
26
+ style={{
27
+ position: "absolute",
28
+ left: x - 30,
29
+ top: y - 30,
30
+ width: 60,
31
+ height: 60,
32
+ borderRadius: "50%",
33
+ border: "2px solid rgba(255,255,255,0.8)",
34
+ transform: `scale(${scale1})`,
35
+ opacity: opacity1,
36
+ }}
37
+ />
38
+ <div
39
+ style={{
40
+ position: "absolute",
41
+ left: x - 8,
42
+ top: y - 8,
43
+ width: 16,
44
+ height: 16,
45
+ borderRadius: "50%",
46
+ background: "rgba(255,255,255,0.9)",
47
+ transform: `scale(${dotScale})`,
48
+ }}
49
+ />
50
+ </>
51
+ );
52
+ };
@@ -0,0 +1,50 @@
1
+ import React, { useState, useEffect } from "react";
2
+
3
+ interface TravelingEvent {
4
+ id: number;
5
+ event: string;
6
+ progress: number;
7
+ target: string;
8
+ direction: "toService" | "toPhone";
9
+ userId: string | null;
10
+ deviceId: string;
11
+ properties: Record<string, any>;
12
+ timestamp: string;
13
+ }
14
+
15
+ /** Animated ball that travels along an SVG path in the live demo */
16
+ export const TravelingBall: React.FC<{
17
+ event: TravelingEvent;
18
+ pathRef: React.RefObject<SVGPathElement>;
19
+ onClick: (e: React.MouseEvent, event: TravelingEvent) => void;
20
+ color: string;
21
+ }> = ({ event, pathRef, onClick, color }) => {
22
+ const [position, setPosition] = useState({ x: 0, y: 0 });
23
+
24
+ useEffect(() => {
25
+ if (!pathRef.current) return;
26
+ const path = pathRef.current;
27
+ const length = path.getTotalLength();
28
+ const point = path.getPointAtLength(length * event.progress);
29
+ setPosition({ x: point.x, y: point.y });
30
+ }, [event.progress, pathRef]);
31
+
32
+ return (
33
+ <g
34
+ style={{ cursor: "pointer", pointerEvents: "all" }}
35
+ onClick={(e) => {
36
+ e.stopPropagation();
37
+ onClick(e as any, event);
38
+ }}
39
+ >
40
+ <circle cx={position.x} cy={position.y} r={40} fill="transparent" style={{ cursor: "pointer" }} />
41
+ <circle cx={position.x} cy={position.y} r={24} fill={color} opacity={0.25}
42
+ style={{ animation: "pulse 1s ease-in-out infinite", transformOrigin: `${position.x}px ${position.y}px` }} />
43
+ <circle cx={position.x} cy={position.y} r={14} fill={color}
44
+ style={{ animation: "ballGlow 1.5s ease-in-out infinite", transformOrigin: `${position.x}px ${position.y}px` }} />
45
+ <circle cx={position.x - 3} cy={position.y - 3} r={4} fill="white" opacity={0.6} />
46
+ </g>
47
+ );
48
+ };
49
+
50
+ export type { TravelingEvent };