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,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
|
+
→ {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
|
+
→ {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 };
|