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 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,16 @@
1
+ node_modules
2
+ dist
3
+ .DS_Store
4
+ .env
5
+
6
+ # Remotion output
7
+ out
8
+
9
+ # Demo build output
10
+ demo-dist
11
+
12
+ # AI editors/tools
13
+ .claude
14
+ .codex
15
+ .cursor
16
+ .gemini
@@ -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,8 @@
1
+ [build]
2
+ command = "npm run demo:build"
3
+ publish = "demo-dist"
4
+
5
+ [[redirects]]
6
+ from = "/*"
7
+ to = "/demo.html"
8
+ status = 200
@@ -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
@@ -0,0 +1,4 @@
1
+ import { Config } from "@remotion/cli/config";
2
+
3
+ Config.setVideoImageFormat("jpeg");
4
+ Config.setOverwriteOutput(true);