@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.
- package/package.json +2 -2
- package/templates/aurora-auth-node-demo/README.md +43 -0
- package/templates/aurora-auth-node-demo/aurora.config.ts +15 -0
- package/templates/aurora-auth-node-demo/bun.lock +679 -0
- package/templates/aurora-auth-node-demo/drizzle.config.ts +9 -0
- package/templates/aurora-auth-node-demo/package.json +39 -0
- package/templates/aurora-auth-node-demo/postcss.config.mjs +7 -0
- package/templates/aurora-auth-node-demo/server.mjs +46 -0
- package/templates/aurora-auth-node-demo/src/actions/createMessage.action.server.ts +31 -0
- package/templates/aurora-auth-node-demo/src/aurora.auth.ts +65 -0
- package/templates/aurora-auth-node-demo/src/lib/auth-client.ts +30 -0
- package/templates/aurora-auth-node-demo/src/lib/auth.server.ts +11 -0
- package/templates/aurora-auth-node-demo/src/lib/auth.ts +30 -0
- package/templates/aurora-auth-node-demo/src/lib/db.ts +6 -0
- package/templates/aurora-auth-node-demo/src/lib/pulse.ts +45 -0
- package/templates/aurora-auth-node-demo/src/lib/schema.ts +107 -0
- package/templates/aurora-auth-node-demo/src/queries/listMessages.server.ts +25 -0
- package/templates/aurora-auth-node-demo/src/routes/api/auth/[...slug]/handler.ts +14 -0
- package/templates/aurora-auth-node-demo/src/routes/api/pulse/verify/handler.ts +55 -0
- package/templates/aurora-auth-node-demo/src/routes/auth/sign-in/page.client.tsx +132 -0
- package/templates/aurora-auth-node-demo/src/routes/auth/sign-in/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/auth/sign-up/page.client.tsx +154 -0
- package/templates/aurora-auth-node-demo/src/routes/auth/sign-up/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/arena-game/page.client.tsx +640 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/arena-game/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/chat/page.client.tsx +349 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/chat/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/encrypted-chat/page.client.tsx +472 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/encrypted-chat/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/game-sync/page.client.tsx +375 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/game-sync/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/queues/page.client.tsx +423 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/queues/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/video-call/page.client.tsx +840 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/video-call/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/watch-together/page.client.tsx +722 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/watch-together/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/layout.client.tsx +113 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/layout.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/page.client.tsx +195 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/favicon.ico +0 -0
- package/templates/aurora-auth-node-demo/src/routes/layout.tsx +18 -0
- package/templates/aurora-auth-node-demo/src/routes/page.client.tsx +263 -0
- package/templates/aurora-auth-node-demo/src/routes/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/styles/app.css +96 -0
- package/templates/aurora-auth-node-demo/tsconfig.json +27 -0
- package/templates/aurora-auth-node-demo/tsconfig.tsbuildinfo +1 -0
- package/templates/nextjs-auth-demo/next-env.d.ts +1 -1
- package/templates/nextjs-auth-demo/package.json +8 -7
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/arena-game/page.tsx +20 -3
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/chat/page.tsx +108 -23
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/encrypted-chat/page.tsx +278 -217
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/game-sync/page.tsx +66 -35
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/queues/page.tsx +213 -87
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/video-call/page.tsx +106 -6
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/watch-together/page.tsx +415 -262
- package/templates/nextjs-auth-node-demo/.env.example +10 -0
- package/templates/nextjs-auth-node-demo/Dockerfile +19 -0
- package/templates/nextjs-auth-node-demo/README.md +159 -0
- package/templates/nextjs-auth-node-demo/_gitignore +33 -0
- package/templates/nextjs-auth-node-demo/drizzle.config.ts +10 -0
- package/templates/nextjs-auth-node-demo/eslint.config.mjs +18 -0
- package/templates/nextjs-auth-node-demo/next-env.d.ts +6 -0
- package/templates/nextjs-auth-node-demo/next.config.ts +7 -0
- package/templates/nextjs-auth-node-demo/package.json +38 -0
- package/templates/nextjs-auth-node-demo/postcss.config.mjs +7 -0
- package/templates/nextjs-auth-node-demo/public/file.svg +1 -0
- package/templates/nextjs-auth-node-demo/public/globe.svg +1 -0
- package/templates/nextjs-auth-node-demo/public/next.svg +1 -0
- package/templates/nextjs-auth-node-demo/public/vercel.svg +1 -0
- package/templates/nextjs-auth-node-demo/public/window.svg +1 -0
- package/templates/nextjs-auth-node-demo/server.mjs +45 -0
- package/templates/nextjs-auth-node-demo/src/app/api/auth/[...all]/route.ts +4 -0
- package/templates/nextjs-auth-node-demo/src/app/api/pulse/verify/route.ts +54 -0
- package/templates/nextjs-auth-node-demo/src/app/auth/sign-in/page.tsx +131 -0
- package/templates/nextjs-auth-node-demo/src/app/auth/sign-up/page.tsx +153 -0
- package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/arena-game/page.tsx +640 -0
- package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/chat/page.tsx +349 -0
- package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/encrypted-chat/page.tsx +472 -0
- package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/game-sync/page.tsx +375 -0
- package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/queues/page.tsx +423 -0
- package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/video-call/page.tsx +840 -0
- package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/watch-together/page.tsx +724 -0
- package/templates/nextjs-auth-node-demo/src/app/dashboard/layout.tsx +113 -0
- package/templates/nextjs-auth-node-demo/src/app/dashboard/page.tsx +195 -0
- package/templates/nextjs-auth-node-demo/src/app/favicon.ico +0 -0
- package/templates/nextjs-auth-node-demo/src/app/globals.css +96 -0
- package/templates/nextjs-auth-node-demo/src/app/layout.tsx +27 -0
- package/templates/nextjs-auth-node-demo/src/app/page.tsx +254 -0
- package/templates/nextjs-auth-node-demo/src/lib/auth-client.ts +15 -0
- package/templates/nextjs-auth-node-demo/src/lib/auth.ts +14 -0
- package/templates/nextjs-auth-node-demo/src/lib/db.ts +6 -0
- package/templates/nextjs-auth-node-demo/src/lib/pulse.ts +45 -0
- package/templates/nextjs-auth-node-demo/src/lib/schema.ts +107 -0
- package/templates/nextjs-auth-node-demo/tsconfig.json +34 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
"use client";
|
|
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
|
+
}
|