@sansavision/create-pulse 0.4.4 → 0.4.6

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 (97) hide show
  1. package/dist/index.js +2 -0
  2. package/package.json +2 -2
  3. package/templates/aurora-auth-node-demo/README.md +43 -0
  4. package/templates/aurora-auth-node-demo/aurora.config.ts +15 -0
  5. package/templates/aurora-auth-node-demo/bun.lock +679 -0
  6. package/templates/aurora-auth-node-demo/drizzle.config.ts +9 -0
  7. package/templates/aurora-auth-node-demo/package.json +39 -0
  8. package/templates/aurora-auth-node-demo/postcss.config.mjs +7 -0
  9. package/templates/aurora-auth-node-demo/server.mjs +46 -0
  10. package/templates/aurora-auth-node-demo/src/actions/createMessage.action.server.ts +31 -0
  11. package/templates/aurora-auth-node-demo/src/aurora.auth.ts +65 -0
  12. package/templates/aurora-auth-node-demo/src/lib/auth-client.ts +30 -0
  13. package/templates/aurora-auth-node-demo/src/lib/auth.server.ts +11 -0
  14. package/templates/aurora-auth-node-demo/src/lib/auth.ts +30 -0
  15. package/templates/aurora-auth-node-demo/src/lib/db.ts +6 -0
  16. package/templates/aurora-auth-node-demo/src/lib/pulse.ts +45 -0
  17. package/templates/aurora-auth-node-demo/src/lib/schema.ts +107 -0
  18. package/templates/aurora-auth-node-demo/src/queries/listMessages.server.ts +25 -0
  19. package/templates/aurora-auth-node-demo/src/routes/api/auth/[...slug]/handler.ts +14 -0
  20. package/templates/aurora-auth-node-demo/src/routes/api/pulse/verify/handler.ts +55 -0
  21. package/templates/aurora-auth-node-demo/src/routes/auth/sign-in/page.client.tsx +132 -0
  22. package/templates/aurora-auth-node-demo/src/routes/auth/sign-in/page.tsx +5 -0
  23. package/templates/aurora-auth-node-demo/src/routes/auth/sign-up/page.client.tsx +154 -0
  24. package/templates/aurora-auth-node-demo/src/routes/auth/sign-up/page.tsx +5 -0
  25. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/arena-game/page.client.tsx +640 -0
  26. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/arena-game/page.tsx +5 -0
  27. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/chat/page.client.tsx +349 -0
  28. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/chat/page.tsx +5 -0
  29. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/encrypted-chat/page.client.tsx +472 -0
  30. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/encrypted-chat/page.tsx +5 -0
  31. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/game-sync/page.client.tsx +375 -0
  32. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/game-sync/page.tsx +5 -0
  33. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/queues/page.client.tsx +423 -0
  34. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/queues/page.tsx +5 -0
  35. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/video-call/page.client.tsx +840 -0
  36. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/video-call/page.tsx +5 -0
  37. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/watch-together/page.client.tsx +722 -0
  38. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/watch-together/page.tsx +5 -0
  39. package/templates/aurora-auth-node-demo/src/routes/dashboard/layout.client.tsx +113 -0
  40. package/templates/aurora-auth-node-demo/src/routes/dashboard/layout.tsx +5 -0
  41. package/templates/aurora-auth-node-demo/src/routes/dashboard/page.client.tsx +195 -0
  42. package/templates/aurora-auth-node-demo/src/routes/dashboard/page.tsx +5 -0
  43. package/templates/aurora-auth-node-demo/src/routes/favicon.ico +0 -0
  44. package/templates/aurora-auth-node-demo/src/routes/layout.tsx +18 -0
  45. package/templates/aurora-auth-node-demo/src/routes/page.client.tsx +263 -0
  46. package/templates/aurora-auth-node-demo/src/routes/page.tsx +5 -0
  47. package/templates/aurora-auth-node-demo/src/styles/app.css +96 -0
  48. package/templates/aurora-auth-node-demo/tsconfig.json +27 -0
  49. package/templates/aurora-auth-node-demo/tsconfig.tsbuildinfo +1 -0
  50. package/templates/nextjs-auth-demo/next-env.d.ts +1 -1
  51. package/templates/nextjs-auth-demo/package.json +8 -7
  52. package/templates/nextjs-auth-demo/src/app/dashboard/demos/arena-game/page.tsx +20 -3
  53. package/templates/nextjs-auth-demo/src/app/dashboard/demos/chat/page.tsx +108 -23
  54. package/templates/nextjs-auth-demo/src/app/dashboard/demos/encrypted-chat/page.tsx +278 -217
  55. package/templates/nextjs-auth-demo/src/app/dashboard/demos/game-sync/page.tsx +66 -35
  56. package/templates/nextjs-auth-demo/src/app/dashboard/demos/queues/page.tsx +213 -87
  57. package/templates/nextjs-auth-demo/src/app/dashboard/demos/video-call/page.tsx +106 -6
  58. package/templates/nextjs-auth-demo/src/app/dashboard/demos/watch-together/page.tsx +415 -262
  59. package/templates/nextjs-auth-node-demo/.env.example +10 -0
  60. package/templates/nextjs-auth-node-demo/Dockerfile +19 -0
  61. package/templates/nextjs-auth-node-demo/README.md +159 -0
  62. package/templates/nextjs-auth-node-demo/_gitignore +33 -0
  63. package/templates/nextjs-auth-node-demo/drizzle.config.ts +10 -0
  64. package/templates/nextjs-auth-node-demo/eslint.config.mjs +18 -0
  65. package/templates/nextjs-auth-node-demo/next-env.d.ts +6 -0
  66. package/templates/nextjs-auth-node-demo/next.config.ts +7 -0
  67. package/templates/nextjs-auth-node-demo/package.json +38 -0
  68. package/templates/nextjs-auth-node-demo/postcss.config.mjs +7 -0
  69. package/templates/nextjs-auth-node-demo/public/file.svg +1 -0
  70. package/templates/nextjs-auth-node-demo/public/globe.svg +1 -0
  71. package/templates/nextjs-auth-node-demo/public/next.svg +1 -0
  72. package/templates/nextjs-auth-node-demo/public/vercel.svg +1 -0
  73. package/templates/nextjs-auth-node-demo/public/window.svg +1 -0
  74. package/templates/nextjs-auth-node-demo/server.mjs +45 -0
  75. package/templates/nextjs-auth-node-demo/src/app/api/auth/[...all]/route.ts +4 -0
  76. package/templates/nextjs-auth-node-demo/src/app/api/pulse/verify/route.ts +54 -0
  77. package/templates/nextjs-auth-node-demo/src/app/auth/sign-in/page.tsx +131 -0
  78. package/templates/nextjs-auth-node-demo/src/app/auth/sign-up/page.tsx +153 -0
  79. package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/arena-game/page.tsx +640 -0
  80. package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/chat/page.tsx +349 -0
  81. package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/encrypted-chat/page.tsx +472 -0
  82. package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/game-sync/page.tsx +375 -0
  83. package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/queues/page.tsx +423 -0
  84. package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/video-call/page.tsx +840 -0
  85. package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/watch-together/page.tsx +724 -0
  86. package/templates/nextjs-auth-node-demo/src/app/dashboard/layout.tsx +113 -0
  87. package/templates/nextjs-auth-node-demo/src/app/dashboard/page.tsx +195 -0
  88. package/templates/nextjs-auth-node-demo/src/app/favicon.ico +0 -0
  89. package/templates/nextjs-auth-node-demo/src/app/globals.css +96 -0
  90. package/templates/nextjs-auth-node-demo/src/app/layout.tsx +27 -0
  91. package/templates/nextjs-auth-node-demo/src/app/page.tsx +254 -0
  92. package/templates/nextjs-auth-node-demo/src/lib/auth-client.ts +15 -0
  93. package/templates/nextjs-auth-node-demo/src/lib/auth.ts +14 -0
  94. package/templates/nextjs-auth-node-demo/src/lib/db.ts +6 -0
  95. package/templates/nextjs-auth-node-demo/src/lib/pulse.ts +45 -0
  96. package/templates/nextjs-auth-node-demo/src/lib/schema.ts +107 -0
  97. package/templates/nextjs-auth-node-demo/tsconfig.json +34 -0
@@ -0,0 +1,349 @@
1
+
2
+
3
+ import { useState, useEffect, useRef, useCallback } from "react";
4
+ import { useSession } from "@/lib/auth-client";
5
+ import { connectWithAuth } from "@/lib/pulse";
6
+ import {
7
+ Send,
8
+ Users,
9
+ Wifi,
10
+ WifiOff,
11
+ Shield,
12
+ Loader2,
13
+ MessageSquare,
14
+ } from "lucide-react";
15
+ import type { PulseConnection } from "@sansavision/pulse-sdk";
16
+
17
+ interface ChatMessage {
18
+ id: string;
19
+ user: string;
20
+ userId: string;
21
+ text: string;
22
+ timestamp: number;
23
+ }
24
+
25
+ export default function ChatDemoPage() {
26
+ const { data: session } = useSession();
27
+ const [connected, setConnected] = useState(false);
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
+ });
37
+ const [input, setInput] = useState("");
38
+ const [authUser, setAuthUser] = useState<{
39
+ id: string;
40
+ claims: Record<string, string>;
41
+ } | null>(null);
42
+ const [onlineUsers, setOnlineUsers] = useState<string[]>([]);
43
+ const [connecting, setConnecting] = useState(true);
44
+ const messagesEndRef = useRef<HTMLDivElement>(null);
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]);
52
+
53
+ const scrollToBottom = useCallback(() => {
54
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
55
+ }, []);
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
64
+ useEffect(() => {
65
+ if (!session) return;
66
+ let cancelled = false;
67
+
68
+ async function init() {
69
+ try {
70
+ const connection = await connectWithAuth();
71
+ if (cancelled) return;
72
+
73
+ connRef.current = connection;
74
+ setConnected(true);
75
+ setConnecting(false);
76
+
77
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
78
+ const user = (connection as any).user;
79
+ if (user) setAuthUser(user);
80
+
81
+ // Open the chat stream ONCE and store in ref
82
+ const stream = connection.stream("chat-room");
83
+ streamRef.current = stream;
84
+
85
+ // Announce join
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
+ }
96
+
97
+ // Listen for messages — uses sessionRef to avoid stale closures
98
+ stream.on("data", (data: Uint8Array) => {
99
+ try {
100
+ const msg = JSON.parse(new TextDecoder().decode(data));
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) {
110
+ setMessages((prev) => [
111
+ ...prev,
112
+ {
113
+ id: msg.msgId || crypto.randomUUID(),
114
+ user: msg.user,
115
+ userId: msg.userId,
116
+ text: msg.text,
117
+ timestamp: Date.now(),
118
+ },
119
+ ]);
120
+ } else if (msg.type === "join" && msg.userId !== selfId) {
121
+ setOnlineUsers((prev) =>
122
+ prev.includes(msg.user) ? prev : [...prev, msg.user]
123
+ );
124
+ setMessages((prev) => [
125
+ ...prev,
126
+ {
127
+ id: crypto.randomUUID(),
128
+ user: "System",
129
+ userId: "system",
130
+ text: `${msg.user} joined the chat`,
131
+ timestamp: Date.now(),
132
+ },
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
+ ]);
160
+ }
161
+ } catch {
162
+ // ignore parse errors
163
+ }
164
+ });
165
+
166
+ connection.on("disconnect", () => setConnected(false));
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
+ });
179
+ } catch (err) {
180
+ console.error("Failed to connect:", err);
181
+ setConnecting(false);
182
+ }
183
+ }
184
+
185
+ init();
186
+
187
+ return () => {
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;
200
+ connRef.current?.disconnect();
201
+ };
202
+ }, [session]);
203
+
204
+ useEffect(scrollToBottom, [messages, scrollToBottom]);
205
+
206
+ function sendMessage(e: React.FormEvent) {
207
+ e.preventDefault();
208
+ if (!input.trim() || !streamRef.current) return;
209
+
210
+ const text = input.trim();
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);
218
+
219
+ streamRef.current.send(
220
+ JSON.stringify({
221
+ type: "chat",
222
+ user,
223
+ userId: usrId,
224
+ text,
225
+ msgId,
226
+ })
227
+ );
228
+
229
+ // Add to local state immediately
230
+ setMessages((prev) => [
231
+ ...prev,
232
+ {
233
+ id: msgId,
234
+ user,
235
+ userId: usrId,
236
+ text,
237
+ timestamp: Date.now(),
238
+ },
239
+ ]);
240
+ setInput("");
241
+ }
242
+
243
+ return (
244
+ <div className="h-screen flex flex-col">
245
+ {/* Header */}
246
+ <div className="p-4 border-b border-slate-800 flex items-center justify-between">
247
+ <div className="flex items-center gap-3">
248
+ <div className="w-10 h-10 rounded-xl bg-gradient-to-br from-blue-500 to-blue-600 flex items-center justify-center">
249
+ <MessageSquare className="w-5 h-5 text-white" />
250
+ </div>
251
+ <div>
252
+ <h1 className="text-lg font-semibold">Real-time Chat</h1>
253
+ <p className="text-xs text-slate-500">
254
+ Multi-user authenticated messaging
255
+ </p>
256
+ </div>
257
+ </div>
258
+ <div className="flex items-center gap-4">
259
+ {authUser && (
260
+ <div className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-green-500/10 border border-green-500/20">
261
+ <Shield className="w-3.5 h-3.5 text-green-400" />
262
+ <span className="text-xs text-green-400">
263
+ {authUser.claims.name || authUser.id}
264
+ </span>
265
+ </div>
266
+ )}
267
+ <div className="flex items-center gap-2">
268
+ {connected ? (
269
+ <>
270
+ <Wifi className="w-4 h-4 text-green-400" />
271
+ <span className="text-xs text-green-400">Connected</span>
272
+ </>
273
+ ) : (
274
+ <>
275
+ <WifiOff className="w-4 h-4 text-red-400" />
276
+ <span className="text-xs text-red-400">Disconnected</span>
277
+ </>
278
+ )}
279
+ </div>
280
+ <div className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-slate-800">
281
+ <Users className="w-3.5 h-3.5 text-slate-400" />
282
+ <span className="text-xs text-slate-400">
283
+ {onlineUsers.length + 1} online
284
+ </span>
285
+ </div>
286
+ </div>
287
+ </div>
288
+
289
+ {/* Messages */}
290
+ <div className="flex-1 overflow-y-auto p-4 space-y-3">
291
+ {connecting && (
292
+ <div className="flex items-center justify-center py-20">
293
+ <Loader2 className="w-6 h-6 animate-spin text-purple-500 mr-2" />
294
+ <span className="text-slate-400">
295
+ Connecting to Pulse relay...
296
+ </span>
297
+ </div>
298
+ )}
299
+
300
+ {messages.map((msg) => (
301
+ <div
302
+ key={msg.id}
303
+ className={`flex ${msg.userId === session?.user?.id ? "justify-end" : "justify-start"
304
+ }`}
305
+ >
306
+ <div
307
+ className={`max-w-md px-4 py-2.5 rounded-2xl text-sm ${msg.userId === "system"
308
+ ? "bg-slate-800/50 text-slate-500 text-center text-xs mx-auto italic"
309
+ : msg.userId === session?.user?.id
310
+ ? "bg-purple-600 text-white rounded-br-md"
311
+ : "glass text-slate-200 rounded-bl-md"
312
+ }`}
313
+ >
314
+ {msg.userId !== "system" && msg.userId !== session?.user?.id && (
315
+ <div className="text-xs text-purple-400 font-medium mb-1">
316
+ {msg.user}
317
+ </div>
318
+ )}
319
+ {msg.text}
320
+ </div>
321
+ </div>
322
+ ))}
323
+ <div ref={messagesEndRef} />
324
+ </div>
325
+
326
+ {/* Input */}
327
+ <form
328
+ onSubmit={sendMessage}
329
+ className="p-4 border-t border-slate-800 flex gap-3"
330
+ >
331
+ <input
332
+ type="text"
333
+ value={input}
334
+ onChange={(e) => setInput(e.target.value)}
335
+ placeholder="Type a message..."
336
+ disabled={!connected}
337
+ className="flex-1 px-4 py-2.5 rounded-xl bg-slate-800/50 border border-slate-700 focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none text-sm transition-colors placeholder:text-slate-600 disabled:opacity-50"
338
+ />
339
+ <button
340
+ type="submit"
341
+ disabled={!connected || !input.trim()}
342
+ className="px-4 py-2.5 bg-purple-600 hover:bg-purple-500 rounded-xl transition-all disabled:opacity-50 disabled:cursor-not-allowed"
343
+ >
344
+ <Send className="w-4 h-4" />
345
+ </button>
346
+ </form>
347
+ </div>
348
+ );
349
+ }
@@ -0,0 +1,5 @@
1
+ import ChatDemoPage from "./page.client";
2
+
3
+ export default function Page() {
4
+ return <ChatDemoPage />;
5
+ }