@sansavision/create-pulse 0.4.4 → 0.4.5

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.
Files changed (96) hide show
  1. package/package.json +2 -2
  2. package/templates/aurora-auth-node-demo/README.md +43 -0
  3. package/templates/aurora-auth-node-demo/aurora.config.ts +15 -0
  4. package/templates/aurora-auth-node-demo/bun.lock +679 -0
  5. package/templates/aurora-auth-node-demo/drizzle.config.ts +9 -0
  6. package/templates/aurora-auth-node-demo/package.json +39 -0
  7. package/templates/aurora-auth-node-demo/postcss.config.mjs +7 -0
  8. package/templates/aurora-auth-node-demo/server.mjs +46 -0
  9. package/templates/aurora-auth-node-demo/src/actions/createMessage.action.server.ts +31 -0
  10. package/templates/aurora-auth-node-demo/src/aurora.auth.ts +65 -0
  11. package/templates/aurora-auth-node-demo/src/lib/auth-client.ts +30 -0
  12. package/templates/aurora-auth-node-demo/src/lib/auth.server.ts +11 -0
  13. package/templates/aurora-auth-node-demo/src/lib/auth.ts +30 -0
  14. package/templates/aurora-auth-node-demo/src/lib/db.ts +6 -0
  15. package/templates/aurora-auth-node-demo/src/lib/pulse.ts +45 -0
  16. package/templates/aurora-auth-node-demo/src/lib/schema.ts +107 -0
  17. package/templates/aurora-auth-node-demo/src/queries/listMessages.server.ts +25 -0
  18. package/templates/aurora-auth-node-demo/src/routes/api/auth/[...slug]/handler.ts +14 -0
  19. package/templates/aurora-auth-node-demo/src/routes/api/pulse/verify/handler.ts +55 -0
  20. package/templates/aurora-auth-node-demo/src/routes/auth/sign-in/page.client.tsx +132 -0
  21. package/templates/aurora-auth-node-demo/src/routes/auth/sign-in/page.tsx +5 -0
  22. package/templates/aurora-auth-node-demo/src/routes/auth/sign-up/page.client.tsx +154 -0
  23. package/templates/aurora-auth-node-demo/src/routes/auth/sign-up/page.tsx +5 -0
  24. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/arena-game/page.client.tsx +640 -0
  25. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/arena-game/page.tsx +5 -0
  26. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/chat/page.client.tsx +349 -0
  27. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/chat/page.tsx +5 -0
  28. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/encrypted-chat/page.client.tsx +472 -0
  29. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/encrypted-chat/page.tsx +5 -0
  30. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/game-sync/page.client.tsx +375 -0
  31. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/game-sync/page.tsx +5 -0
  32. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/queues/page.client.tsx +423 -0
  33. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/queues/page.tsx +5 -0
  34. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/video-call/page.client.tsx +840 -0
  35. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/video-call/page.tsx +5 -0
  36. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/watch-together/page.client.tsx +722 -0
  37. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/watch-together/page.tsx +5 -0
  38. package/templates/aurora-auth-node-demo/src/routes/dashboard/layout.client.tsx +113 -0
  39. package/templates/aurora-auth-node-demo/src/routes/dashboard/layout.tsx +5 -0
  40. package/templates/aurora-auth-node-demo/src/routes/dashboard/page.client.tsx +195 -0
  41. package/templates/aurora-auth-node-demo/src/routes/dashboard/page.tsx +5 -0
  42. package/templates/aurora-auth-node-demo/src/routes/favicon.ico +0 -0
  43. package/templates/aurora-auth-node-demo/src/routes/layout.tsx +18 -0
  44. package/templates/aurora-auth-node-demo/src/routes/page.client.tsx +263 -0
  45. package/templates/aurora-auth-node-demo/src/routes/page.tsx +5 -0
  46. package/templates/aurora-auth-node-demo/src/styles/app.css +96 -0
  47. package/templates/aurora-auth-node-demo/tsconfig.json +27 -0
  48. package/templates/aurora-auth-node-demo/tsconfig.tsbuildinfo +1 -0
  49. package/templates/nextjs-auth-demo/next-env.d.ts +1 -1
  50. package/templates/nextjs-auth-demo/package.json +8 -7
  51. package/templates/nextjs-auth-demo/src/app/dashboard/demos/arena-game/page.tsx +20 -3
  52. package/templates/nextjs-auth-demo/src/app/dashboard/demos/chat/page.tsx +108 -23
  53. package/templates/nextjs-auth-demo/src/app/dashboard/demos/encrypted-chat/page.tsx +278 -217
  54. package/templates/nextjs-auth-demo/src/app/dashboard/demos/game-sync/page.tsx +66 -35
  55. package/templates/nextjs-auth-demo/src/app/dashboard/demos/queues/page.tsx +213 -87
  56. package/templates/nextjs-auth-demo/src/app/dashboard/demos/video-call/page.tsx +106 -6
  57. package/templates/nextjs-auth-demo/src/app/dashboard/demos/watch-together/page.tsx +415 -262
  58. package/templates/nextjs-auth-node-demo/.env.example +10 -0
  59. package/templates/nextjs-auth-node-demo/Dockerfile +19 -0
  60. package/templates/nextjs-auth-node-demo/README.md +159 -0
  61. package/templates/nextjs-auth-node-demo/_gitignore +33 -0
  62. package/templates/nextjs-auth-node-demo/drizzle.config.ts +10 -0
  63. package/templates/nextjs-auth-node-demo/eslint.config.mjs +18 -0
  64. package/templates/nextjs-auth-node-demo/next-env.d.ts +6 -0
  65. package/templates/nextjs-auth-node-demo/next.config.ts +7 -0
  66. package/templates/nextjs-auth-node-demo/package.json +38 -0
  67. package/templates/nextjs-auth-node-demo/postcss.config.mjs +7 -0
  68. package/templates/nextjs-auth-node-demo/public/file.svg +1 -0
  69. package/templates/nextjs-auth-node-demo/public/globe.svg +1 -0
  70. package/templates/nextjs-auth-node-demo/public/next.svg +1 -0
  71. package/templates/nextjs-auth-node-demo/public/vercel.svg +1 -0
  72. package/templates/nextjs-auth-node-demo/public/window.svg +1 -0
  73. package/templates/nextjs-auth-node-demo/server.mjs +45 -0
  74. package/templates/nextjs-auth-node-demo/src/app/api/auth/[...all]/route.ts +4 -0
  75. package/templates/nextjs-auth-node-demo/src/app/api/pulse/verify/route.ts +54 -0
  76. package/templates/nextjs-auth-node-demo/src/app/auth/sign-in/page.tsx +131 -0
  77. package/templates/nextjs-auth-node-demo/src/app/auth/sign-up/page.tsx +153 -0
  78. package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/arena-game/page.tsx +640 -0
  79. package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/chat/page.tsx +349 -0
  80. package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/encrypted-chat/page.tsx +472 -0
  81. package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/game-sync/page.tsx +375 -0
  82. package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/queues/page.tsx +423 -0
  83. package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/video-call/page.tsx +840 -0
  84. package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/watch-together/page.tsx +724 -0
  85. package/templates/nextjs-auth-node-demo/src/app/dashboard/layout.tsx +113 -0
  86. package/templates/nextjs-auth-node-demo/src/app/dashboard/page.tsx +195 -0
  87. package/templates/nextjs-auth-node-demo/src/app/favicon.ico +0 -0
  88. package/templates/nextjs-auth-node-demo/src/app/globals.css +96 -0
  89. package/templates/nextjs-auth-node-demo/src/app/layout.tsx +27 -0
  90. package/templates/nextjs-auth-node-demo/src/app/page.tsx +254 -0
  91. package/templates/nextjs-auth-node-demo/src/lib/auth-client.ts +15 -0
  92. package/templates/nextjs-auth-node-demo/src/lib/auth.ts +14 -0
  93. package/templates/nextjs-auth-node-demo/src/lib/db.ts +6 -0
  94. package/templates/nextjs-auth-node-demo/src/lib/pulse.ts +45 -0
  95. package/templates/nextjs-auth-node-demo/src/lib/schema.ts +107 -0
  96. package/templates/nextjs-auth-node-demo/tsconfig.json +34 -0
@@ -1,6 +1,6 @@
1
1
  /// <reference types="next" />
2
2
  /// <reference types="next/image-types/global" />
3
- import "./.next/types/routes.d.ts";
3
+ import "./.next/dev/types/routes.d.ts";
4
4
 
5
5
  // NOTE: This file should not be edited
6
6
  // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
@@ -12,25 +12,26 @@
12
12
  "db:studio": "npx drizzle-kit studio"
13
13
  },
14
14
  "dependencies": {
15
- "next": "16.1.6",
16
- "react": "19.2.3",
17
- "react-dom": "19.2.3",
15
+ "@sansavision/pulse-sdk": "^0.4.2",
18
16
  "better-auth": "^1.2.0",
19
17
  "better-sqlite3": "^12.0.0",
20
18
  "drizzle-orm": "^0.41.0",
21
- "@sansavision/pulse-sdk": "^0.4.2",
22
- "lucide-react": "^0.412.0"
19
+ "lucide-react": "^0.412.0",
20
+ "next": "16.1.6",
21
+ "react": "19.2.3",
22
+ "react-dom": "19.2.3",
23
+ "react-player": "^2.16.0"
23
24
  },
24
25
  "devDependencies": {
25
26
  "@tailwindcss/postcss": "^4",
27
+ "@types/better-sqlite3": "^7.6.0",
26
28
  "@types/node": "^20",
27
29
  "@types/react": "^19",
28
30
  "@types/react-dom": "^19",
29
- "@types/better-sqlite3": "^7.6.0",
30
31
  "drizzle-kit": "^0.31.0",
31
32
  "eslint": "^9",
32
33
  "eslint-config-next": "16.1.6",
33
34
  "tailwindcss": "^4",
34
35
  "typescript": "^5"
35
36
  }
36
- }
37
+ }
@@ -16,6 +16,7 @@ import {
16
16
  Trophy,
17
17
  Zap,
18
18
  Activity,
19
+ AlertCircle,
19
20
  } from "lucide-react";
20
21
  import type { PulseConnection } from "@sansavision/pulse-sdk";
21
22
 
@@ -47,6 +48,7 @@ export default function ArenaGamePage() {
47
48
  // Room management
48
49
  const [roomId, setRoomId] = useState("");
49
50
  const [inputRoomId, setInputRoomId] = useState("");
51
+ const [error, setError] = useState("");
50
52
  const [inGame, setInGame] = useState(false);
51
53
  const [copied, setCopied] = useState(false);
52
54
  const [playerCount, setPlayerCount] = useState(1);
@@ -401,8 +403,16 @@ export default function ArenaGamePage() {
401
403
  // Join existing room
402
404
  function handleJoinRoom(e: React.FormEvent) {
403
405
  e.preventDefault();
404
- if (!inputRoomId.trim()) return;
405
- joinGame(inputRoomId.trim());
406
+ const cleaned = inputRoomId.trim();
407
+ if (!cleaned) return;
408
+
409
+ if (!/^[a-zA-Z0-9-]{6,12}$/.test(cleaned)) {
410
+ setError("Invalid room format. Must be 6-12 characters (letters, numbers, hyphens).");
411
+ return;
412
+ }
413
+
414
+ setError("");
415
+ joinGame(cleaned);
406
416
  }
407
417
 
408
418
  // Copy room ID
@@ -493,11 +503,18 @@ export default function ArenaGamePage() {
493
503
  </div>
494
504
  </div>
495
505
 
506
+ {error && (
507
+ <div className="mb-4 p-3 rounded-lg bg-red-500/10 border border-red-500/20 flex items-center gap-2 text-red-400 text-sm text-left">
508
+ <AlertCircle className="w-4 h-4 shrink-0" />
509
+ <p>{error}</p>
510
+ </div>
511
+ )}
512
+
496
513
  <form onSubmit={handleJoinRoom} className="flex gap-3">
497
514
  <input
498
515
  type="text"
499
516
  value={inputRoomId}
500
- onChange={(e) => setInputRoomId(e.target.value)}
517
+ onChange={(e) => { setInputRoomId(e.target.value); setError(""); }}
501
518
  placeholder="Enter room code..."
502
519
  className="flex-1 px-4 py-2.5 rounded-xl bg-slate-800/50 border border-slate-700 focus:border-orange-500 focus:ring-1 focus:ring-orange-500 outline-none text-sm transition-colors placeholder:text-slate-600"
503
520
  />
@@ -25,7 +25,15 @@ interface ChatMessage {
25
25
  export default function ChatDemoPage() {
26
26
  const { data: session } = useSession();
27
27
  const [connected, setConnected] = useState(false);
28
- const [messages, setMessages] = useState<ChatMessage[]>([]);
28
+ const [messages, setMessages] = useState<ChatMessage[]>(() => {
29
+ if (typeof window !== "undefined") {
30
+ try {
31
+ const stored = localStorage.getItem("pulse-chat-messages");
32
+ if (stored) return JSON.parse(stored);
33
+ } catch { /* ignore */ }
34
+ }
35
+ return [];
36
+ });
29
37
  const [input, setInput] = useState("");
30
38
  const [authUser, setAuthUser] = useState<{
31
39
  id: string;
@@ -35,11 +43,24 @@ export default function ChatDemoPage() {
35
43
  const [connecting, setConnecting] = useState(true);
36
44
  const messagesEndRef = useRef<HTMLDivElement>(null);
37
45
  const connRef = useRef<PulseConnection | null>(null);
46
+ const streamRef = useRef<ReturnType<PulseConnection["stream"]> | null>(null);
47
+ const sessionRef = useRef(session);
48
+ const seenIdsRef = useRef(new Set<string>());
49
+
50
+ // Keep session ref in sync
51
+ useEffect(() => { sessionRef.current = session; }, [session]);
38
52
 
39
53
  const scrollToBottom = useCallback(() => {
40
54
  messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
41
55
  }, []);
42
56
 
57
+ useEffect(() => {
58
+ if (typeof window !== "undefined") {
59
+ localStorage.setItem("pulse-chat-messages", JSON.stringify(messages.slice(-100)));
60
+ }
61
+ }, [messages]);
62
+
63
+ // Connect to relay — run ONCE when session is available
43
64
  useEffect(() => {
44
65
  if (!session) return;
45
66
  let cancelled = false;
@@ -53,39 +74,50 @@ export default function ChatDemoPage() {
53
74
  setConnected(true);
54
75
  setConnecting(false);
55
76
 
56
- // Check auth user from the ACCEPT payload
57
77
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
58
78
  const user = (connection as any).user;
59
79
  if (user) setAuthUser(user);
60
80
 
61
- // Open a chat stream
81
+ // Open the chat stream ONCE and store in ref
62
82
  const stream = connection.stream("chat-room");
83
+ streamRef.current = stream;
63
84
 
64
85
  // Announce join
65
- stream.send(
66
- JSON.stringify({
67
- type: "join",
68
- user: session!.user.name,
69
- userId: session!.user.id,
70
- })
71
- );
86
+ const sess = sessionRef.current;
87
+ if (sess) {
88
+ stream.send(
89
+ JSON.stringify({
90
+ type: "join",
91
+ user: sess.user.name,
92
+ userId: sess.user.id,
93
+ })
94
+ );
95
+ }
72
96
 
73
- // Listen for messages (skip self-echoes we add our own messages locally)
97
+ // Listen for messages — uses sessionRef to avoid stale closures
74
98
  stream.on("data", (data: Uint8Array) => {
75
99
  try {
76
100
  const msg = JSON.parse(new TextDecoder().decode(data));
77
- if (msg.type === "chat" && msg.userId !== session!.user.id) {
101
+ const currentSess = sessionRef.current;
102
+ if (!currentSess) return;
103
+ const selfId = currentSess.user.id;
104
+
105
+ // Deduplication via message ID
106
+ if (msg.msgId && seenIdsRef.current.has(msg.msgId)) return;
107
+ if (msg.msgId) seenIdsRef.current.add(msg.msgId);
108
+
109
+ if (msg.type === "chat" && msg.userId !== selfId) {
78
110
  setMessages((prev) => [
79
111
  ...prev,
80
112
  {
81
- id: crypto.randomUUID(),
113
+ id: msg.msgId || crypto.randomUUID(),
82
114
  user: msg.user,
83
115
  userId: msg.userId,
84
116
  text: msg.text,
85
117
  timestamp: Date.now(),
86
118
  },
87
119
  ]);
88
- } else if (msg.type === "join") {
120
+ } else if (msg.type === "join" && msg.userId !== selfId) {
89
121
  setOnlineUsers((prev) =>
90
122
  prev.includes(msg.user) ? prev : [...prev, msg.user]
91
123
  );
@@ -99,6 +131,32 @@ export default function ChatDemoPage() {
99
131
  timestamp: Date.now(),
100
132
  },
101
133
  ]);
134
+
135
+ // Announce our presence back
136
+ const s = sessionRef.current;
137
+ if (s) {
138
+ stream.send(JSON.stringify({
139
+ type: "presence",
140
+ user: s.user.name,
141
+ userId: s.user.id,
142
+ }));
143
+ }
144
+ } else if (msg.type === "presence" && msg.userId !== selfId) {
145
+ setOnlineUsers((prev) =>
146
+ prev.includes(msg.user) ? prev : [...prev, msg.user]
147
+ );
148
+ } else if (msg.type === "leave" && msg.userId !== selfId) {
149
+ setOnlineUsers((prev) => prev.filter(u => u !== msg.user));
150
+ setMessages((prev) => [
151
+ ...prev,
152
+ {
153
+ id: crypto.randomUUID(),
154
+ user: "System",
155
+ userId: "system",
156
+ text: `${msg.user} left the chat`,
157
+ timestamp: Date.now(),
158
+ },
159
+ ]);
102
160
  }
103
161
  } catch {
104
162
  // ignore parse errors
@@ -106,7 +164,18 @@ export default function ChatDemoPage() {
106
164
  });
107
165
 
108
166
  connection.on("disconnect", () => setConnected(false));
109
- connection.on("reconnected", () => setConnected(true));
167
+ connection.on("reconnected", () => {
168
+ setConnected(true);
169
+ // Re-announce on reconnect
170
+ const s = sessionRef.current;
171
+ if (s && streamRef.current) {
172
+ streamRef.current.send(JSON.stringify({
173
+ type: "join",
174
+ user: s.user.name,
175
+ userId: s.user.id,
176
+ }));
177
+ }
178
+ });
110
179
  } catch (err) {
111
180
  console.error("Failed to connect:", err);
112
181
  setConnecting(false);
@@ -117,6 +186,17 @@ export default function ChatDemoPage() {
117
186
 
118
187
  return () => {
119
188
  cancelled = true;
189
+ const sess = sessionRef.current;
190
+ if (streamRef.current && sess) {
191
+ try {
192
+ streamRef.current.send(JSON.stringify({
193
+ type: "leave",
194
+ user: sess.user.name,
195
+ userId: sess.user.id
196
+ }));
197
+ } catch { /* ignore */ }
198
+ }
199
+ streamRef.current = null;
120
200
  connRef.current?.disconnect();
121
201
  };
122
202
  }, [session]);
@@ -125,27 +205,32 @@ export default function ChatDemoPage() {
125
205
 
126
206
  function sendMessage(e: React.FormEvent) {
127
207
  e.preventDefault();
128
- if (!input.trim() || !connRef.current) return;
208
+ if (!input.trim() || !streamRef.current) return;
129
209
 
130
210
  const text = input.trim();
131
- const user = session?.user?.name || "Anonymous";
132
- const usrId = session?.user?.id || "unknown";
211
+ const sess = sessionRef.current;
212
+ const user = sess?.user?.name || "Anonymous";
213
+ const usrId = sess?.user?.id || "unknown";
214
+ const msgId = crypto.randomUUID();
215
+
216
+ // Mark as seen so we don't add our own echo
217
+ seenIdsRef.current.add(msgId);
133
218
 
134
- const stream = connRef.current.stream("chat-room");
135
- stream.send(
219
+ streamRef.current.send(
136
220
  JSON.stringify({
137
221
  type: "chat",
138
222
  user,
139
223
  userId: usrId,
140
224
  text,
225
+ msgId,
141
226
  })
142
227
  );
143
228
 
144
- // Add to local state immediately (relay echoes, but we filter self in on("data"))
229
+ // Add to local state immediately
145
230
  setMessages((prev) => [
146
231
  ...prev,
147
232
  {
148
- id: crypto.randomUUID(),
233
+ id: msgId,
149
234
  user,
150
235
  userId: usrId,
151
236
  text,
@@ -195,7 +280,7 @@ export default function ChatDemoPage() {
195
280
  <div className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-slate-800">
196
281
  <Users className="w-3.5 h-3.5 text-slate-400" />
197
282
  <span className="text-xs text-slate-400">
198
- {onlineUsers.length || 1} online
283
+ {onlineUsers.length + 1} online
199
284
  </span>
200
285
  </div>
201
286
  </div>