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
package/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# create-analytics-demo
|
|
2
|
+
|
|
3
|
+
Scaffold a real-time analytics event visualization (WebSocket + browser).
|
|
4
|
+
|
|
5
|
+
Shows how events flow from your app to analytics services (Amplitude, Braze, etc.) with animated paths, code snippets, and an event history sidebar.
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx create-analytics-demo my-project
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The CLI will ask for:
|
|
14
|
+
- **App name** — displayed in the UI
|
|
15
|
+
- **Accent color** — hex color for the brand theme
|
|
16
|
+
|
|
17
|
+
Use `--defaults` / `-y` to skip prompts.
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
cd my-project
|
|
23
|
+
npm install
|
|
24
|
+
cd demo-server && npm install && cd ..
|
|
25
|
+
npm run demo
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
This opens `http://localhost:3000` with the visualization. Then send events from your app or with curl:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
curl -X POST http://localhost:3001/event \
|
|
32
|
+
-H "Content-Type: application/json" \
|
|
33
|
+
-d '{
|
|
34
|
+
"event": "login completed",
|
|
35
|
+
"userId": "u_123",
|
|
36
|
+
"deviceId": "d_abc",
|
|
37
|
+
"properties": {"method": "email"}
|
|
38
|
+
}'
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
You'll see the event travel as an animated ball from the phone to the service logos. Click on a ball or sidebar item to see the Swift code snippet.
|
|
42
|
+
|
|
43
|
+
## Customization
|
|
44
|
+
|
|
45
|
+
Edit `src/demo.config.ts` to change:
|
|
46
|
+
|
|
47
|
+
- **App name and theme** — colors, branding
|
|
48
|
+
- **Services** — names, colors, logos (put images in `public/`)
|
|
49
|
+
- **Event categories** — maps event names to display categories
|
|
50
|
+
- **Secondary service events** — which events also go to the second service
|
|
51
|
+
- **Code snippets** — Swift code shown when clicking an event
|
|
52
|
+
- **Server/demo ports**
|
|
53
|
+
|
|
54
|
+
## Project structure
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
my-project/
|
|
58
|
+
├── demo-server/
|
|
59
|
+
│ └── server.js # Express + WebSocket server
|
|
60
|
+
├── src/
|
|
61
|
+
│ ├── demo.config.ts # ← Edit this to customize everything
|
|
62
|
+
│ ├── components/ # IPhoneFrame, TravelingBall, LiveCodePanel, etc.
|
|
63
|
+
│ └── demo/
|
|
64
|
+
│ └── DemoApp.tsx # Main visualization
|
|
65
|
+
├── public/ # Service logos go here
|
|
66
|
+
└── demo.html
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## How it works
|
|
70
|
+
|
|
71
|
+
1. **demo-server** receives POST requests at `/event` and broadcasts them via WebSocket
|
|
72
|
+
2. **DemoApp** connects to the WebSocket and visualizes each event as an animated ball traveling along SVG paths from the phone to the service logos
|
|
73
|
+
3. Click any event to see the corresponding code snippet
|
|
74
|
+
4. The sidebar shows a scrollable event history
|
|
75
|
+
|
|
76
|
+
## Deploy
|
|
77
|
+
|
|
78
|
+
Pre-configured for Vercel (`vercel.json`) and Netlify (`netlify.toml`). Just connect your repo.
|
|
79
|
+
|
|
80
|
+
For the WebSocket server, deploy `demo-server/` separately (e.g. Railway, Render) and update `demo.wsUrl` in `demo.config.ts`.
|
package/index.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const prompts = require("prompts");
|
|
6
|
+
const ejs = require("ejs");
|
|
7
|
+
const chalk = require("chalk");
|
|
8
|
+
|
|
9
|
+
const TEMPLATE_DIR = path.join(__dirname, "template");
|
|
10
|
+
|
|
11
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
function copyDir(src, dest, ejsVars) {
|
|
14
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
15
|
+
|
|
16
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
17
|
+
const srcPath = path.join(src, entry.name);
|
|
18
|
+
let destName = entry.name;
|
|
19
|
+
|
|
20
|
+
// _gitignore → .gitignore
|
|
21
|
+
if (destName === "_gitignore") destName = ".gitignore";
|
|
22
|
+
|
|
23
|
+
const destPath = path.join(dest, destName);
|
|
24
|
+
|
|
25
|
+
if (entry.isDirectory()) {
|
|
26
|
+
copyDir(srcPath, destPath, ejsVars);
|
|
27
|
+
} else if (entry.name.endsWith(".ejs")) {
|
|
28
|
+
// Process EJS template
|
|
29
|
+
const template = fs.readFileSync(srcPath, "utf-8");
|
|
30
|
+
const rendered = ejs.render(template, ejsVars);
|
|
31
|
+
const finalName = destName.replace(/\.ejs$/, "");
|
|
32
|
+
fs.writeFileSync(path.join(dest, finalName), rendered);
|
|
33
|
+
} else {
|
|
34
|
+
// Copy as-is
|
|
35
|
+
fs.copyFileSync(srcPath, destPath);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function hexToRgba(hex, alpha) {
|
|
41
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
42
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
43
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
44
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function lightenHex(hex, amount) {
|
|
48
|
+
let r = parseInt(hex.slice(1, 3), 16);
|
|
49
|
+
let g = parseInt(hex.slice(3, 5), 16);
|
|
50
|
+
let b = parseInt(hex.slice(5, 7), 16);
|
|
51
|
+
r = Math.min(255, r + amount);
|
|
52
|
+
g = Math.min(255, g + amount);
|
|
53
|
+
b = Math.min(255, b + amount);
|
|
54
|
+
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── Main ────────────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
async function main() {
|
|
60
|
+
const args = process.argv.slice(2);
|
|
61
|
+
const useDefaults = args.includes("--defaults") || args.includes("-y");
|
|
62
|
+
const projectName = args.find((a) => !a.startsWith("-"));
|
|
63
|
+
|
|
64
|
+
if (!projectName) {
|
|
65
|
+
console.log(chalk.red("Usage: npx create-analytics-demo <project-name>"));
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const targetDir = path.resolve(process.cwd(), projectName);
|
|
70
|
+
|
|
71
|
+
if (fs.existsSync(targetDir)) {
|
|
72
|
+
console.log(chalk.red(`Directory "${projectName}" already exists.`));
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
console.log();
|
|
77
|
+
console.log(chalk.bold(" create-analytics-demo"));
|
|
78
|
+
console.log(chalk.gray(" Real-time analytics visualization (Remotion + WebSocket)"));
|
|
79
|
+
console.log();
|
|
80
|
+
|
|
81
|
+
let appName, accentColor;
|
|
82
|
+
|
|
83
|
+
if (useDefaults) {
|
|
84
|
+
appName = projectName
|
|
85
|
+
.replace(/[-_]/g, " ")
|
|
86
|
+
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
87
|
+
accentColor = "#FC5B40";
|
|
88
|
+
} else {
|
|
89
|
+
const response = await prompts(
|
|
90
|
+
[
|
|
91
|
+
{
|
|
92
|
+
type: "text",
|
|
93
|
+
name: "appName",
|
|
94
|
+
message: "App name",
|
|
95
|
+
initial: projectName
|
|
96
|
+
.replace(/[-_]/g, " ")
|
|
97
|
+
.replace(/\b\w/g, (c) => c.toUpperCase()),
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
type: "text",
|
|
101
|
+
name: "accentColor",
|
|
102
|
+
message: "Accent color (hex)",
|
|
103
|
+
initial: "#FC5B40",
|
|
104
|
+
validate: (v) =>
|
|
105
|
+
/^#[0-9a-fA-F]{6}$/.test(v) || "Enter a valid hex color (e.g. #FC5B40)",
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
{ onCancel: () => process.exit(0) }
|
|
109
|
+
);
|
|
110
|
+
appName = response.appName;
|
|
111
|
+
accentColor = response.accentColor;
|
|
112
|
+
}
|
|
113
|
+
const accentSoft = lightenHex(accentColor, 30);
|
|
114
|
+
|
|
115
|
+
// Template variables
|
|
116
|
+
const vars = {
|
|
117
|
+
projectName,
|
|
118
|
+
appName,
|
|
119
|
+
accentColor,
|
|
120
|
+
accentSoft,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
console.log();
|
|
124
|
+
console.log(chalk.cyan(" Scaffolding project..."));
|
|
125
|
+
|
|
126
|
+
// Copy template → target
|
|
127
|
+
copyDir(TEMPLATE_DIR, targetDir, vars);
|
|
128
|
+
|
|
129
|
+
// Make start-demo.sh executable
|
|
130
|
+
const startScript = path.join(targetDir, "start-demo.sh");
|
|
131
|
+
if (fs.existsSync(startScript)) {
|
|
132
|
+
fs.chmodSync(startScript, 0o755);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
console.log(chalk.green(" ✔ Project created in ./" + projectName));
|
|
136
|
+
console.log();
|
|
137
|
+
console.log(chalk.bold(" Next steps:"));
|
|
138
|
+
console.log();
|
|
139
|
+
console.log(chalk.cyan(` cd ${projectName}`));
|
|
140
|
+
console.log(chalk.cyan(" npm install"));
|
|
141
|
+
console.log(chalk.cyan(" cd demo-server && npm install && cd .."));
|
|
142
|
+
console.log(chalk.cyan(" npm run demo"));
|
|
143
|
+
console.log();
|
|
144
|
+
console.log(chalk.gray(" Then send events:"));
|
|
145
|
+
console.log(
|
|
146
|
+
chalk.gray(
|
|
147
|
+
' curl -X POST http://localhost:3001/event -H "Content-Type: application/json" \\'
|
|
148
|
+
)
|
|
149
|
+
);
|
|
150
|
+
console.log(
|
|
151
|
+
chalk.gray(
|
|
152
|
+
` -d '{"event":"login completed","userId":"u_123","deviceId":"d_abc","properties":{"method":"email"}}'`
|
|
153
|
+
)
|
|
154
|
+
);
|
|
155
|
+
console.log();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
main().catch((err) => {
|
|
159
|
+
console.error(chalk.red(err.message));
|
|
160
|
+
process.exit(1);
|
|
161
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-analytics-demo",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Scaffold a real-time analytics visualization project (Remotion + WebSocket)",
|
|
5
|
+
"bin": {
|
|
6
|
+
"create-analytics-demo": "index.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"index.js",
|
|
10
|
+
"template/**"
|
|
11
|
+
],
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"chalk": "^4.1.2",
|
|
14
|
+
"ejs": "^3.1.10",
|
|
15
|
+
"prompts": "^2.4.2"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "<%= projectName %>-demo-server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "WebSocket server for <%= appName %> analytics demo",
|
|
5
|
+
"main": "server.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"start": "node server.js",
|
|
8
|
+
"dev": "node --watch server.js"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"cors": "^2.8.5",
|
|
12
|
+
"express": "^4.18.2",
|
|
13
|
+
"ws": "^8.16.0"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const cors = require('cors');
|
|
3
|
+
const { WebSocketServer } = require('ws');
|
|
4
|
+
const http = require('http');
|
|
5
|
+
|
|
6
|
+
const app = express();
|
|
7
|
+
const PORT = 3001;
|
|
8
|
+
|
|
9
|
+
// Middleware
|
|
10
|
+
app.use(cors());
|
|
11
|
+
app.use(express.json());
|
|
12
|
+
|
|
13
|
+
// Create HTTP server
|
|
14
|
+
const server = http.createServer(app);
|
|
15
|
+
|
|
16
|
+
// Create WebSocket server
|
|
17
|
+
const wss = new WebSocketServer({ server });
|
|
18
|
+
|
|
19
|
+
// Store connected clients
|
|
20
|
+
const clients = new Set();
|
|
21
|
+
|
|
22
|
+
// WebSocket connection handling
|
|
23
|
+
wss.on('connection', (ws) => {
|
|
24
|
+
console.log('Browser connected');
|
|
25
|
+
clients.add(ws);
|
|
26
|
+
|
|
27
|
+
// Send welcome message
|
|
28
|
+
ws.send(JSON.stringify({
|
|
29
|
+
type: 'connected',
|
|
30
|
+
message: 'Connected to <%= appName %> Demo Server',
|
|
31
|
+
timestamp: new Date().toISOString()
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
ws.on('close', () => {
|
|
35
|
+
console.log('Browser disconnected');
|
|
36
|
+
clients.delete(ws);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
ws.on('error', (error) => {
|
|
40
|
+
console.error('WebSocket error:', error);
|
|
41
|
+
clients.delete(ws);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Broadcast to all connected clients
|
|
46
|
+
function broadcast(data) {
|
|
47
|
+
const message = JSON.stringify(data);
|
|
48
|
+
clients.forEach((client) => {
|
|
49
|
+
if (client.readyState === 1) { // WebSocket.OPEN
|
|
50
|
+
client.send(message);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// HTTP endpoint to receive events from app
|
|
56
|
+
app.post('/event', (req, res) => {
|
|
57
|
+
const event = {
|
|
58
|
+
type: 'analytics_event',
|
|
59
|
+
...req.body,
|
|
60
|
+
receivedAt: new Date().toISOString()
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
console.log(`Event received: ${event.event}`);
|
|
64
|
+
console.log(` userId: ${event.userId || 'anonymous'}`);
|
|
65
|
+
console.log(` deviceId: ${event.deviceId}`);
|
|
66
|
+
if (event.properties) {
|
|
67
|
+
console.log(` properties:`, event.properties);
|
|
68
|
+
}
|
|
69
|
+
console.log('');
|
|
70
|
+
|
|
71
|
+
// Broadcast to all connected browsers
|
|
72
|
+
broadcast(event);
|
|
73
|
+
|
|
74
|
+
res.status(200).json({ success: true });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Health check endpoint
|
|
78
|
+
app.get('/health', (req, res) => {
|
|
79
|
+
res.json({
|
|
80
|
+
status: 'ok',
|
|
81
|
+
clients: clients.size,
|
|
82
|
+
timestamp: new Date().toISOString()
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Reset endpoint (to clear state in browser)
|
|
87
|
+
app.post('/reset', (req, res) => {
|
|
88
|
+
console.log('Reset requested');
|
|
89
|
+
broadcast({ type: 'reset', timestamp: new Date().toISOString() });
|
|
90
|
+
res.json({ success: true });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Start server
|
|
94
|
+
server.listen(PORT, () => {
|
|
95
|
+
console.log('');
|
|
96
|
+
console.log('='.repeat(55));
|
|
97
|
+
console.log(' <%= appName %> Demo Server');
|
|
98
|
+
console.log('='.repeat(55));
|
|
99
|
+
console.log('');
|
|
100
|
+
console.log(` HTTP endpoint: http://localhost:${PORT}/event`);
|
|
101
|
+
console.log(` WebSocket: ws://localhost:${PORT}`);
|
|
102
|
+
console.log(` Health check: http://localhost:${PORT}/health`);
|
|
103
|
+
console.log('');
|
|
104
|
+
console.log(' Waiting for connections...');
|
|
105
|
+
console.log('');
|
|
106
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title><%= appName %> Analytics - Live Demo</title>
|
|
7
|
+
<style>
|
|
8
|
+
* {
|
|
9
|
+
margin: 0;
|
|
10
|
+
padding: 0;
|
|
11
|
+
box-sizing: border-box;
|
|
12
|
+
}
|
|
13
|
+
body {
|
|
14
|
+
background: #0a0a12;
|
|
15
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
16
|
+
overflow: hidden;
|
|
17
|
+
}
|
|
18
|
+
#root {
|
|
19
|
+
width: 100vw;
|
|
20
|
+
height: 100vh;
|
|
21
|
+
}
|
|
22
|
+
</style>
|
|
23
|
+
</head>
|
|
24
|
+
<body>
|
|
25
|
+
<div id="root"></div>
|
|
26
|
+
<script type="module" src="/src/demo/main.tsx"></script>
|
|
27
|
+
</body>
|
|
28
|
+
</html>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "<%= projectName %>",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "<%= appName %> Analytics Video",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "remotion studio",
|
|
7
|
+
"build": "remotion bundle",
|
|
8
|
+
"render": "remotion render MyComp out.mp4",
|
|
9
|
+
"upgrade": "remotion upgrade",
|
|
10
|
+
"demo": "vite --config vite.demo.config.ts",
|
|
11
|
+
"demo:build": "vite build --config vite.demo.config.ts"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@remotion/cli": "^4.0.0",
|
|
15
|
+
"@remotion/player": "^4.0.0",
|
|
16
|
+
"react": "^18.2.0",
|
|
17
|
+
"react-dom": "^18.2.0",
|
|
18
|
+
"remotion": "^4.0.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/react": "^18.2.0",
|
|
22
|
+
"@vitejs/plugin-react": "^4.2.1",
|
|
23
|
+
"typescript": "^5.0.0",
|
|
24
|
+
"vite": "^5.0.0"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
File without changes
|