@sansavision/create-pulse 0.4.3 → 0.4.4
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 +1 -1
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/templates/nextjs-auth-demo/README.md +1 -1
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/arena-game/page.tsx +623 -0
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/chat/page.tsx +21 -5
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/encrypted-chat/page.tsx +220 -7
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/game-sync/page.tsx +199 -47
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/video-call/page.tsx +740 -0
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/watch-together/page.tsx +364 -51
- package/templates/nextjs-auth-demo/src/app/dashboard/layout.tsx +4 -0
- package/templates/nextjs-auth-demo/src/app/dashboard/page.tsx +53 -5
- package/templates/nextjs-auth-demo/src/app/layout.tsx +1 -1
|
@@ -70,11 +70,11 @@ export default function ChatDemoPage() {
|
|
|
70
70
|
})
|
|
71
71
|
);
|
|
72
72
|
|
|
73
|
-
// Listen for messages
|
|
73
|
+
// Listen for messages (skip self-echoes — we add our own messages locally)
|
|
74
74
|
stream.on("data", (data: Uint8Array) => {
|
|
75
75
|
try {
|
|
76
76
|
const msg = JSON.parse(new TextDecoder().decode(data));
|
|
77
|
-
if (msg.type === "chat") {
|
|
77
|
+
if (msg.type === "chat" && msg.userId !== session!.user.id) {
|
|
78
78
|
setMessages((prev) => [
|
|
79
79
|
...prev,
|
|
80
80
|
{
|
|
@@ -127,15 +127,31 @@ export default function ChatDemoPage() {
|
|
|
127
127
|
e.preventDefault();
|
|
128
128
|
if (!input.trim() || !connRef.current) return;
|
|
129
129
|
|
|
130
|
+
const text = input.trim();
|
|
131
|
+
const user = session?.user?.name || "Anonymous";
|
|
132
|
+
const usrId = session?.user?.id || "unknown";
|
|
133
|
+
|
|
130
134
|
const stream = connRef.current.stream("chat-room");
|
|
131
135
|
stream.send(
|
|
132
136
|
JSON.stringify({
|
|
133
137
|
type: "chat",
|
|
134
|
-
user
|
|
135
|
-
userId:
|
|
136
|
-
text
|
|
138
|
+
user,
|
|
139
|
+
userId: usrId,
|
|
140
|
+
text,
|
|
137
141
|
})
|
|
138
142
|
);
|
|
143
|
+
|
|
144
|
+
// Add to local state immediately (relay echoes, but we filter self in on("data"))
|
|
145
|
+
setMessages((prev) => [
|
|
146
|
+
...prev,
|
|
147
|
+
{
|
|
148
|
+
id: crypto.randomUUID(),
|
|
149
|
+
user,
|
|
150
|
+
userId: usrId,
|
|
151
|
+
text,
|
|
152
|
+
timestamp: Date.now(),
|
|
153
|
+
},
|
|
154
|
+
]);
|
|
139
155
|
setInput("");
|
|
140
156
|
}
|
|
141
157
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { useState, useEffect, useRef, useCallback } from "react";
|
|
4
4
|
import { useSession } from "@/lib/auth-client";
|
|
5
5
|
import { connectWithAuth } from "@/lib/pulse";
|
|
6
|
-
import { Lock, Send, Shield, Wifi, WifiOff, Loader2, Key, ShieldCheck, Eye, EyeOff } from "lucide-react";
|
|
6
|
+
import { Lock, Send, Shield, Wifi, WifiOff, Loader2, Key, ShieldCheck, Eye, EyeOff, Code, X, ChevronDown } from "lucide-react";
|
|
7
7
|
import type { PulseConnection } from "@sansavision/pulse-sdk";
|
|
8
8
|
|
|
9
9
|
interface EncryptedMessage {
|
|
@@ -12,8 +12,26 @@ interface EncryptedMessage {
|
|
|
12
12
|
plaintext: string;
|
|
13
13
|
ciphertext: string;
|
|
14
14
|
timestamp: number;
|
|
15
|
+
type: "text" | "code";
|
|
16
|
+
language?: string;
|
|
15
17
|
}
|
|
16
18
|
|
|
19
|
+
const CODE_LANGUAGES = [
|
|
20
|
+
{ value: "javascript", label: "JavaScript" },
|
|
21
|
+
{ value: "typescript", label: "TypeScript" },
|
|
22
|
+
{ value: "python", label: "Python" },
|
|
23
|
+
{ value: "rust", label: "Rust" },
|
|
24
|
+
{ value: "go", label: "Go" },
|
|
25
|
+
{ value: "java", label: "Java" },
|
|
26
|
+
{ value: "css", label: "CSS" },
|
|
27
|
+
{ value: "html", label: "HTML" },
|
|
28
|
+
{ value: "sql", label: "SQL" },
|
|
29
|
+
{ value: "bash", label: "Bash" },
|
|
30
|
+
{ value: "json", label: "JSON" },
|
|
31
|
+
{ value: "yaml", label: "YAML" },
|
|
32
|
+
{ value: "plaintext", label: "Plain Text" },
|
|
33
|
+
];
|
|
34
|
+
|
|
17
35
|
export default function EncryptedChatPage() {
|
|
18
36
|
const { data: session } = useSession();
|
|
19
37
|
const [connected, setConnected] = useState(false);
|
|
@@ -22,12 +40,17 @@ export default function EncryptedChatPage() {
|
|
|
22
40
|
const [messages, setMessages] = useState<EncryptedMessage[]>([]);
|
|
23
41
|
const [input, setInput] = useState("");
|
|
24
42
|
const [showCiphertext, setShowCiphertext] = useState(false);
|
|
43
|
+
const [showCodeEditor, setShowCodeEditor] = useState(false);
|
|
44
|
+
const [codeContent, setCodeContent] = useState("");
|
|
45
|
+
const [codeLanguage, setCodeLanguage] = useState("javascript");
|
|
46
|
+
const [showLangDropdown, setShowLangDropdown] = useState(false);
|
|
25
47
|
const [encryptionKey] = useState(() =>
|
|
26
48
|
Array.from(crypto.getRandomValues(new Uint8Array(16)))
|
|
27
49
|
.map((b) => b.toString(16).padStart(2, "0")).join("")
|
|
28
50
|
);
|
|
29
51
|
const connRef = useRef<PulseConnection | null>(null);
|
|
30
52
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
53
|
+
const codeTextareaRef = useRef<HTMLTextAreaElement>(null);
|
|
31
54
|
|
|
32
55
|
const scrollToBottom = useCallback(() => {
|
|
33
56
|
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
@@ -65,10 +88,12 @@ export default function EncryptedChatPage() {
|
|
|
65
88
|
|
|
66
89
|
const stream = connection.stream("encrypted-room");
|
|
67
90
|
|
|
91
|
+
// Listen for messages (skip self-echoes — we add our own messages locally)
|
|
68
92
|
stream.on("data", (data: Uint8Array) => {
|
|
69
93
|
try {
|
|
70
94
|
const msg = JSON.parse(new TextDecoder().decode(data));
|
|
71
|
-
|
|
95
|
+
const selfName = session?.user?.name || "Anonymous";
|
|
96
|
+
if ((msg.type === "encrypted-msg" || msg.type === "encrypted-code") && msg.sender !== selfName) {
|
|
72
97
|
const decrypted = simpleDecrypt(msg.ciphertext, encryptionKey);
|
|
73
98
|
setMessages((prev) => [
|
|
74
99
|
...prev,
|
|
@@ -78,6 +103,8 @@ export default function EncryptedChatPage() {
|
|
|
78
103
|
plaintext: decrypted,
|
|
79
104
|
ciphertext: msg.ciphertext,
|
|
80
105
|
timestamp: Date.now(),
|
|
106
|
+
type: msg.type === "encrypted-code" ? "code" : "text",
|
|
107
|
+
language: msg.language,
|
|
81
108
|
},
|
|
82
109
|
]);
|
|
83
110
|
}
|
|
@@ -108,12 +135,87 @@ export default function EncryptedChatPage() {
|
|
|
108
135
|
|
|
109
136
|
const plaintext = input.trim();
|
|
110
137
|
const ciphertext = simpleEncrypt(plaintext, encryptionKey);
|
|
138
|
+
const sender = session?.user?.name || "Anonymous";
|
|
111
139
|
|
|
112
140
|
const stream = connRef.current.stream("encrypted-room");
|
|
113
|
-
stream.send(JSON.stringify({ type: "encrypted-msg", sender
|
|
141
|
+
stream.send(JSON.stringify({ type: "encrypted-msg", sender, ciphertext }));
|
|
142
|
+
|
|
143
|
+
// Add to local state immediately (relay echoes, but we filter self in on("data"))
|
|
144
|
+
setMessages((prev) => [
|
|
145
|
+
...prev,
|
|
146
|
+
{
|
|
147
|
+
id: crypto.randomUUID(),
|
|
148
|
+
sender,
|
|
149
|
+
plaintext,
|
|
150
|
+
ciphertext,
|
|
151
|
+
timestamp: Date.now(),
|
|
152
|
+
type: "text",
|
|
153
|
+
},
|
|
154
|
+
]);
|
|
114
155
|
setInput("");
|
|
115
156
|
}
|
|
116
157
|
|
|
158
|
+
function sendCode() {
|
|
159
|
+
if (!codeContent.trim() || !connRef.current) return;
|
|
160
|
+
|
|
161
|
+
const plaintext = codeContent.trim();
|
|
162
|
+
const ciphertext = simpleEncrypt(plaintext, encryptionKey);
|
|
163
|
+
const sender = session?.user?.name || "Anonymous";
|
|
164
|
+
|
|
165
|
+
const stream = connRef.current.stream("encrypted-room");
|
|
166
|
+
stream.send(JSON.stringify({
|
|
167
|
+
type: "encrypted-code",
|
|
168
|
+
sender,
|
|
169
|
+
ciphertext,
|
|
170
|
+
language: codeLanguage,
|
|
171
|
+
}));
|
|
172
|
+
|
|
173
|
+
// Add to local state immediately
|
|
174
|
+
setMessages((prev) => [
|
|
175
|
+
...prev,
|
|
176
|
+
{
|
|
177
|
+
id: crypto.randomUUID(),
|
|
178
|
+
sender,
|
|
179
|
+
plaintext,
|
|
180
|
+
ciphertext,
|
|
181
|
+
timestamp: Date.now(),
|
|
182
|
+
type: "code",
|
|
183
|
+
language: codeLanguage,
|
|
184
|
+
},
|
|
185
|
+
]);
|
|
186
|
+
setCodeContent("");
|
|
187
|
+
setShowCodeEditor(false);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function handleCodeKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
|
|
191
|
+
// Handle Tab key for indentation
|
|
192
|
+
if (e.key === "Tab") {
|
|
193
|
+
e.preventDefault();
|
|
194
|
+
const textarea = e.currentTarget;
|
|
195
|
+
const start = textarea.selectionStart;
|
|
196
|
+
const end = textarea.selectionEnd;
|
|
197
|
+
const newValue = codeContent.substring(0, start) + " " + codeContent.substring(end);
|
|
198
|
+
setCodeContent(newValue);
|
|
199
|
+
// Set cursor position after the tab
|
|
200
|
+
setTimeout(() => {
|
|
201
|
+
textarea.selectionStart = textarea.selectionEnd = start + 2;
|
|
202
|
+
}, 0);
|
|
203
|
+
}
|
|
204
|
+
// Cmd/Ctrl+Enter to send
|
|
205
|
+
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
|
206
|
+
e.preventDefault();
|
|
207
|
+
sendCode();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function openCodeEditor() {
|
|
212
|
+
setShowCodeEditor(true);
|
|
213
|
+
setTimeout(() => codeTextareaRef.current?.focus(), 100);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const getLanguageLabel = (val: string) =>
|
|
217
|
+
CODE_LANGUAGES.find((l) => l.value === val)?.label || val;
|
|
218
|
+
|
|
117
219
|
return (
|
|
118
220
|
<div className="h-screen flex flex-col">
|
|
119
221
|
<div className="p-4 border-b border-slate-800">
|
|
@@ -169,10 +271,37 @@ export default function EncryptedChatPage() {
|
|
|
169
271
|
{messages.map((msg) => (
|
|
170
272
|
<div key={msg.id}>
|
|
171
273
|
<div className={`flex ${msg.sender === session?.user?.name ? "justify-end" : "justify-start"}`}>
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
<div className=
|
|
175
|
-
|
|
274
|
+
{msg.type === "code" ? (
|
|
275
|
+
/* Code snippet message */
|
|
276
|
+
<div className={`max-w-lg w-full rounded-2xl overflow-hidden border ${msg.sender === session?.user?.name
|
|
277
|
+
? "border-purple-500/30 bg-purple-950/30"
|
|
278
|
+
: "border-slate-700 bg-slate-800/50"
|
|
279
|
+
}`}>
|
|
280
|
+
<div className="flex items-center justify-between px-4 py-2 border-b border-slate-700/50 bg-slate-900/50">
|
|
281
|
+
<div className="flex items-center gap-2">
|
|
282
|
+
<Code className="w-3.5 h-3.5 text-purple-400" />
|
|
283
|
+
<span className="text-xs font-medium text-purple-400">
|
|
284
|
+
{getLanguageLabel(msg.language || "plaintext")}
|
|
285
|
+
</span>
|
|
286
|
+
</div>
|
|
287
|
+
<div className="flex items-center gap-2">
|
|
288
|
+
<Lock className="w-3 h-3 text-green-400" />
|
|
289
|
+
{msg.sender !== session?.user?.name && (
|
|
290
|
+
<span className="text-xs text-slate-400">{msg.sender}</span>
|
|
291
|
+
)}
|
|
292
|
+
</div>
|
|
293
|
+
</div>
|
|
294
|
+
<pre className="p-4 text-sm font-mono overflow-x-auto text-slate-200 leading-relaxed">
|
|
295
|
+
<code>{msg.plaintext}</code>
|
|
296
|
+
</pre>
|
|
297
|
+
</div>
|
|
298
|
+
) : (
|
|
299
|
+
/* Normal text message */
|
|
300
|
+
<div className={`max-w-md px-4 py-2.5 rounded-2xl text-sm ${msg.sender === session?.user?.name ? "bg-purple-600 text-white rounded-br-md" : "glass text-slate-200 rounded-bl-md"}`}>
|
|
301
|
+
{msg.sender !== session?.user?.name && <div className="text-xs text-purple-400 font-medium mb-1">{msg.sender}</div>}
|
|
302
|
+
<div className="flex items-center gap-1.5"><Lock className="w-3 h-3 text-green-400 shrink-0" />{msg.plaintext}</div>
|
|
303
|
+
</div>
|
|
304
|
+
)}
|
|
176
305
|
</div>
|
|
177
306
|
{showCiphertext && (
|
|
178
307
|
<div className={`mt-1 text-xs font-mono text-slate-600 ${msg.sender === session?.user?.name ? "text-right" : "text-left"} px-4`}>
|
|
@@ -184,7 +313,91 @@ export default function EncryptedChatPage() {
|
|
|
184
313
|
<div ref={messagesEndRef} />
|
|
185
314
|
</div>
|
|
186
315
|
|
|
316
|
+
{/* Code Editor Panel */}
|
|
317
|
+
{showCodeEditor && (
|
|
318
|
+
<div className="border-t border-slate-800 bg-slate-900/80 backdrop-blur">
|
|
319
|
+
<div className="flex items-center justify-between px-4 py-2 border-b border-slate-800">
|
|
320
|
+
<div className="flex items-center gap-3">
|
|
321
|
+
<div className="flex items-center gap-2">
|
|
322
|
+
<Code className="w-4 h-4 text-purple-400" />
|
|
323
|
+
<span className="text-sm font-medium text-purple-400">Share Code Snippet</span>
|
|
324
|
+
</div>
|
|
325
|
+
{/* Language selector */}
|
|
326
|
+
<div className="relative">
|
|
327
|
+
<button
|
|
328
|
+
onClick={() => setShowLangDropdown(!showLangDropdown)}
|
|
329
|
+
className="flex items-center gap-1.5 px-3 py-1 rounded-lg bg-slate-800 border border-slate-700 hover:border-slate-600 transition-colors text-xs"
|
|
330
|
+
>
|
|
331
|
+
{getLanguageLabel(codeLanguage)}
|
|
332
|
+
<ChevronDown className="w-3 h-3 text-slate-400" />
|
|
333
|
+
</button>
|
|
334
|
+
{showLangDropdown && (
|
|
335
|
+
<div className="absolute bottom-full left-0 mb-1 w-44 max-h-52 overflow-y-auto rounded-lg bg-slate-800 border border-slate-700 shadow-xl z-50">
|
|
336
|
+
{CODE_LANGUAGES.map((lang) => (
|
|
337
|
+
<button
|
|
338
|
+
key={lang.value}
|
|
339
|
+
onClick={() => { setCodeLanguage(lang.value); setShowLangDropdown(false); }}
|
|
340
|
+
className={`w-full text-left px-3 py-2 text-xs hover:bg-slate-700 transition-colors ${codeLanguage === lang.value ? "text-purple-400 bg-purple-500/10" : "text-slate-300"
|
|
341
|
+
}`}
|
|
342
|
+
>
|
|
343
|
+
{lang.label}
|
|
344
|
+
</button>
|
|
345
|
+
))}
|
|
346
|
+
</div>
|
|
347
|
+
)}
|
|
348
|
+
</div>
|
|
349
|
+
</div>
|
|
350
|
+
<div className="flex items-center gap-2">
|
|
351
|
+
<span className="text-[10px] text-slate-500">⌘+Enter to send</span>
|
|
352
|
+
<button
|
|
353
|
+
onClick={() => { setShowCodeEditor(false); setCodeContent(""); }}
|
|
354
|
+
className="p-1 rounded hover:bg-slate-800 transition-colors"
|
|
355
|
+
>
|
|
356
|
+
<X className="w-4 h-4 text-slate-400" />
|
|
357
|
+
</button>
|
|
358
|
+
</div>
|
|
359
|
+
</div>
|
|
360
|
+
<div className="p-3">
|
|
361
|
+
<textarea
|
|
362
|
+
ref={codeTextareaRef}
|
|
363
|
+
value={codeContent}
|
|
364
|
+
onChange={(e) => setCodeContent(e.target.value)}
|
|
365
|
+
onKeyDown={handleCodeKeyDown}
|
|
366
|
+
placeholder="Paste or type your code here..."
|
|
367
|
+
spellCheck={false}
|
|
368
|
+
className="w-full h-32 px-4 py-3 rounded-xl bg-slate-950 border border-slate-700 focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none text-sm font-mono text-slate-200 resize-none transition-colors placeholder:text-slate-600 leading-relaxed"
|
|
369
|
+
/>
|
|
370
|
+
<div className="flex justify-end gap-2 mt-2">
|
|
371
|
+
<button
|
|
372
|
+
onClick={() => { setShowCodeEditor(false); setCodeContent(""); }}
|
|
373
|
+
className="px-4 py-2 rounded-lg text-xs text-slate-400 hover:bg-slate-800 transition-colors"
|
|
374
|
+
>
|
|
375
|
+
Cancel
|
|
376
|
+
</button>
|
|
377
|
+
<button
|
|
378
|
+
onClick={sendCode}
|
|
379
|
+
disabled={!connected || !codeContent.trim()}
|
|
380
|
+
className="px-4 py-2 rounded-lg bg-purple-600 hover:bg-purple-500 text-xs font-semibold transition-all disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1.5"
|
|
381
|
+
>
|
|
382
|
+
<Lock className="w-3 h-3" />
|
|
383
|
+
Send Encrypted
|
|
384
|
+
</button>
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
</div>
|
|
388
|
+
)}
|
|
389
|
+
|
|
390
|
+
{/* Message input */}
|
|
187
391
|
<form onSubmit={sendMessage} className="p-4 border-t border-slate-800 flex gap-3">
|
|
392
|
+
<button
|
|
393
|
+
type="button"
|
|
394
|
+
onClick={openCodeEditor}
|
|
395
|
+
disabled={!connected}
|
|
396
|
+
className="px-3 py-2.5 rounded-xl bg-slate-800/50 border border-slate-700 hover:border-purple-500 hover:bg-purple-500/10 transition-all disabled:opacity-50 disabled:cursor-not-allowed group"
|
|
397
|
+
title="Share code snippet"
|
|
398
|
+
>
|
|
399
|
+
<Code className="w-4 h-4 text-slate-400 group-hover:text-purple-400 transition-colors" />
|
|
400
|
+
</button>
|
|
188
401
|
<input type="text" value={input} onChange={(e) => setInput(e.target.value)} placeholder="Type an encrypted message..."
|
|
189
402
|
disabled={!connected}
|
|
190
403
|
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" />
|