@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,423 @@
1
+
2
+
3
+ import { useState, useEffect, useRef } from "react";
4
+ import { useSession } from "@/lib/auth-client";
5
+ import { connectWithAuth } from "@/lib/pulse";
6
+ import {
7
+ Database, Send, Download, Check, WifiOff, Wifi, Shield,
8
+ Loader2, Trash2, Clock, RefreshCw, Layers, Lock
9
+ } from "lucide-react";
10
+ import type { PulseConnection } from "@sansavision/pulse-sdk";
11
+ import { PulseQueue } from "@sansavision/pulse-sdk";
12
+
13
+ interface QueueMsg {
14
+ id: number;
15
+ payload: string;
16
+ timestamp: number;
17
+ acked: boolean;
18
+ }
19
+
20
+ export default function QueueDemoPage() {
21
+ const { data: session } = useSession();
22
+ const [connected, setConnected] = useState(false);
23
+ const [authUser, setAuthUser] = useState<{ id: string; claims: Record<string, string> } | null>(null);
24
+ const [connecting, setConnecting] = useState(true);
25
+ const [simulatedOffline, setSimulatedOffline] = useState(false);
26
+ const [publishInput, setPublishInput] = useState("");
27
+ const [queueType, setQueueType] = useState<"public" | "private">("public");
28
+ const [publishedMessages, setPublishedMessages] = useState<QueueMsg[]>(() => {
29
+ if (typeof window !== 'undefined') {
30
+ const saved = localStorage.getItem('pulse-queue-published');
31
+ if (saved) return JSON.parse(saved);
32
+ }
33
+ return [];
34
+ });
35
+ const [receivedMessages, setReceivedMessages] = useState<QueueMsg[]>(() => {
36
+ if (typeof window !== 'undefined') {
37
+ const saved = localStorage.getItem('pulse-queue-received');
38
+ if (saved) return JSON.parse(saved);
39
+ }
40
+ return [];
41
+ });
42
+ const connRef = useRef<PulseConnection | null>(null);
43
+ const [outbox, setOutbox] = useState<{ payload: string; queueName: string; timestamp: number }[]>(() => {
44
+ if (typeof window !== 'undefined') {
45
+ const saved = localStorage.getItem('pulse-queue-outbox');
46
+ if (saved) return JSON.parse(saved);
47
+ }
48
+ return [];
49
+ });
50
+
51
+ const getQueueName = () => {
52
+ if (queueType === "public") return "demo-queue-public";
53
+ return `demo-queue-private-${session?.user?.id || "unknown"}`;
54
+ };
55
+
56
+ const getQueue = () => {
57
+ if (!connRef.current) return null;
58
+ return new PulseQueue(connRef.current, getQueueName());
59
+ };
60
+
61
+ // Persist outbox
62
+ useEffect(() => {
63
+ localStorage.setItem('pulse-queue-outbox', JSON.stringify(outbox));
64
+ }, [outbox]);
65
+
66
+ useEffect(() => {
67
+ if (!session) return;
68
+ let cancelled = false;
69
+
70
+ async function init() {
71
+ try {
72
+ const connection = await connectWithAuth();
73
+ if (cancelled) return;
74
+
75
+ connRef.current = connection;
76
+ setConnected(true);
77
+ setConnecting(false);
78
+
79
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
80
+ const user = (connection as any).user;
81
+ if (user) setAuthUser(user);
82
+
83
+ connection.on("disconnect", () => setConnected(false));
84
+ connection.on("reconnected", () => {
85
+ setConnected(true);
86
+ // Auto-flush outbox on reconnect
87
+ flushOutbox(connection);
88
+ });
89
+ } catch (err) {
90
+ console.error("Failed to connect:", err);
91
+ setConnecting(false);
92
+ }
93
+ }
94
+
95
+ init();
96
+
97
+ return () => {
98
+ cancelled = true;
99
+ connRef.current?.disconnect();
100
+ };
101
+ }, [session]);
102
+
103
+ // Flush outbox when connection becomes available
104
+ async function flushOutbox(conn?: PulseConnection) {
105
+ const connection = conn || connRef.current;
106
+ if (!connection) return;
107
+ const pending = JSON.parse(localStorage.getItem('pulse-queue-outbox') || '[]');
108
+ if (pending.length === 0) return;
109
+
110
+ const flushed: number[] = [];
111
+ for (let i = 0; i < pending.length; i++) {
112
+ const item = pending[i];
113
+ try {
114
+ const q = new PulseQueue(connection, item.queueName);
115
+ const result = await q.publish(item.payload, { ttlSecs: 300 });
116
+ setPublishedMessages((prev) => [
117
+ ...prev,
118
+ {
119
+ id: result.sequence || Date.now(),
120
+ payload: item.payload,
121
+ timestamp: item.timestamp,
122
+ acked: false,
123
+ },
124
+ ]);
125
+ flushed.push(i);
126
+ } catch (err) {
127
+ console.error('Outbox flush failed for item:', err);
128
+ }
129
+ }
130
+ // Remove flushed items
131
+ const remaining = pending.filter((_: unknown, idx: number) => !flushed.includes(idx));
132
+ setOutbox(remaining);
133
+ }
134
+
135
+ // Auto-flush outbox when we first connect
136
+ useEffect(() => {
137
+ if (connected && outbox.length > 0) {
138
+ flushOutbox();
139
+ }
140
+ // eslint-disable-next-line react-hooks/exhaustive-deps
141
+ }, [connected]);
142
+
143
+ // Persist messages
144
+ useEffect(() => {
145
+ localStorage.setItem('pulse-queue-published', JSON.stringify(publishedMessages.slice(-50)));
146
+ }, [publishedMessages]);
147
+
148
+ useEffect(() => {
149
+ localStorage.setItem('pulse-queue-received', JSON.stringify(receivedMessages.slice(-50)));
150
+ }, [receivedMessages]);
151
+
152
+ async function handlePublish(e: React.FormEvent) {
153
+ e.preventDefault();
154
+ if (!publishInput.trim()) return;
155
+
156
+ const payload = publishInput.trim();
157
+ const qName = getQueueName();
158
+
159
+ // If offline, queue locally
160
+ if (!connected || !connRef.current) {
161
+ setOutbox((prev) => [...prev, { payload, queueName: qName, timestamp: Date.now() }]);
162
+ setPublishedMessages((prev) => [
163
+ ...prev,
164
+ {
165
+ id: -(Date.now()), // negative ID = pending/local
166
+ payload: `⏳ ${payload}`,
167
+ timestamp: Date.now(),
168
+ acked: false,
169
+ },
170
+ ]);
171
+ setPublishInput("");
172
+ return;
173
+ }
174
+
175
+ const q = getQueue();
176
+ if (!q) return;
177
+
178
+ try {
179
+ const result = await q.publish(payload, { ttlSecs: 300 });
180
+
181
+ setPublishedMessages((prev) => [
182
+ ...prev,
183
+ {
184
+ id: result.sequence || Date.now(),
185
+ payload,
186
+ timestamp: Date.now(),
187
+ acked: false,
188
+ },
189
+ ]);
190
+ setPublishInput("");
191
+ } catch (err) {
192
+ console.error("Publish failed:", err);
193
+ }
194
+ }
195
+
196
+ async function handleConsume() {
197
+ const q = getQueue();
198
+ if (!q) return;
199
+
200
+ try {
201
+ const msg = await q.pull();
202
+ if (msg) {
203
+ setReceivedMessages((prev) => {
204
+ if (prev.find(m => m.id === msg.sequence)) return prev;
205
+ return [
206
+ ...prev,
207
+ {
208
+ id: msg.sequence,
209
+ payload: msg.payload,
210
+ timestamp: Date.now(),
211
+ acked: false,
212
+ },
213
+ ];
214
+ });
215
+ }
216
+ } catch (err) {
217
+ console.error("Consume failed:", err);
218
+ }
219
+ }
220
+
221
+ async function handleDrainAll() {
222
+ const q = getQueue();
223
+ if (!q) return;
224
+
225
+ try {
226
+ const msgs = await q.drain(50);
227
+ const mapped = msgs.map((m) => ({
228
+ id: m.sequence,
229
+ payload: m.payload,
230
+ timestamp: Date.now(),
231
+ acked: false,
232
+ }));
233
+
234
+ setReceivedMessages((prev) => {
235
+ const map = new Map(prev.map(p => [p.id, p]));
236
+ for (const m of mapped) { map.set(m.id, m); }
237
+ return Array.from(map.values()).sort((a, b) => a.id - b.id);
238
+ });
239
+ } catch (err) {
240
+ console.error("Drain failed:", err);
241
+ }
242
+ }
243
+
244
+ async function handleAck(sequence: number) {
245
+ const q = getQueue();
246
+ if (!q) return;
247
+
248
+ try {
249
+ await q.ack(sequence);
250
+ setReceivedMessages((prev) =>
251
+ prev.map((m) => (m.id === sequence ? { ...m, acked: true } : m))
252
+ );
253
+ } catch {
254
+ // Message expired or no longer in relay — mark as stale in UI
255
+ setReceivedMessages((prev) =>
256
+ prev.map((m) => (m.id === sequence ? { ...m, acked: true, payload: m.payload + " (expired)" } : m))
257
+ );
258
+ }
259
+ }
260
+
261
+ function toggleOffline() {
262
+ if (simulatedOffline) {
263
+ setSimulatedOffline(false);
264
+ async function reconnect() {
265
+ try {
266
+ const connection = await connectWithAuth();
267
+ connRef.current = connection;
268
+ setConnected(true);
269
+ } catch {
270
+ console.error("Reconnect failed");
271
+ }
272
+ }
273
+ reconnect();
274
+ } else {
275
+ setSimulatedOffline(true);
276
+ connRef.current?.disconnect();
277
+ setConnected(false);
278
+ }
279
+ }
280
+
281
+ return (
282
+ <div className="p-8">
283
+ <div className="flex items-center justify-between mb-8">
284
+ <div className="flex items-center gap-3">
285
+ <div className="w-12 h-12 rounded-xl bg-gradient-to-br from-amber-500 to-orange-600 flex items-center justify-center">
286
+ <Database className="w-6 h-6 text-white" />
287
+ </div>
288
+ <div>
289
+ <h1 className="text-2xl font-bold">Durable Queues</h1>
290
+ <p className="text-sm text-slate-500">
291
+ Persistent store-and-forward with multiple topologies
292
+ </p>
293
+ </div>
294
+ </div>
295
+ <div className="flex items-center gap-3">
296
+ {authUser && (
297
+ <div className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-green-500/10 border border-green-500/20">
298
+ <Shield className="w-3.5 h-3.5 text-green-400" />
299
+ <span className="text-xs text-green-400">{authUser.claims.name || authUser.id}</span>
300
+ </div>
301
+ )}
302
+ <div className="flex items-center gap-2">
303
+ {connected ? (
304
+ <><div className="status-online" /><span className="text-xs text-green-400">Online</span></>
305
+ ) : (
306
+ <><div className="status-offline" /><span className="text-xs text-red-400">{simulatedOffline ? "Simulated Offline" : "Disconnected"}</span></>
307
+ )}
308
+ </div>
309
+ </div>
310
+ </div>
311
+
312
+ {connecting ? (
313
+ <div className="flex items-center justify-center py-20">
314
+ <Loader2 className="w-6 h-6 animate-spin text-purple-500 mr-2" />
315
+ <span className="text-slate-400">Connecting to Pulse relay...</span>
316
+ </div>
317
+ ) : (
318
+ <>
319
+ <div className="flex flex-col md:flex-row gap-4 mb-8">
320
+ <div className="glass rounded-xl p-5 flex-1 flex items-center justify-between">
321
+ <div>
322
+ <h3 className="text-sm font-semibold mb-1 flex items-center gap-2">
323
+ {queueType === 'public' ? <Layers className="w-4 h-4 text-cyan-400" /> : <Lock className="w-4 h-4 text-amber-400" />}
324
+ Queue Topology
325
+ </h3>
326
+ <p className="text-xs text-slate-400">
327
+ {queueType === 'public' ? "Public queue: all demo users can push and pull." : "Private queue: only you can access these messages."}
328
+ </p>
329
+ </div>
330
+ <div className="flex bg-slate-900 rounded-lg p-1 border border-slate-800">
331
+ <button onClick={() => { setQueueType("public"); setReceivedMessages([]); setPublishedMessages([]); }} className={`px-4 py-1.5 rounded-md text-sm font-medium transition-colors ${queueType === 'public' ? 'bg-slate-700 text-white' : 'text-slate-400 hover:text-white'}`}>Public</button>
332
+ <button onClick={() => { setQueueType("private"); setReceivedMessages([]); setPublishedMessages([]); }} className={`px-4 py-1.5 rounded-md text-sm font-medium transition-colors ${queueType === 'private' ? 'bg-slate-700 text-white' : 'text-slate-400 hover:text-white'}`}>Private</button>
333
+ </div>
334
+ </div>
335
+ <div className="glass rounded-xl p-5 flex-1 flex items-center justify-between">
336
+ <div>
337
+ <h3 className="text-sm font-semibold mb-1">🧪 Offline Simulation</h3>
338
+ <p className="text-xs text-slate-400">
339
+ Simulate a network disconnect. Messages will queue on the relay.
340
+ </p>
341
+ </div>
342
+ <button onClick={toggleOffline}
343
+ className={`flex items-center gap-2 px-5 py-2.5 rounded-lg font-medium text-sm transition-all ${simulatedOffline
344
+ ? "bg-green-600 hover:bg-green-500 text-white"
345
+ : "bg-red-600/20 border border-red-500/30 text-red-400 hover:bg-red-600/30"
346
+ }`}>
347
+ {simulatedOffline ? (<><Wifi className="w-4 h-4" />Resume</>) : (<><WifiOff className="w-4 h-4" />Go Offline</>)}
348
+ </button>
349
+ </div>
350
+ </div>
351
+
352
+ <div className="grid lg:grid-cols-2 gap-8">
353
+ {/* Publisher */}
354
+ <div className="glass rounded-2xl p-6 border-t border-slate-700/50">
355
+ <h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
356
+ <Send className="w-4 h-4 text-amber-400" />Publisher <span className="text-xs font-mono text-slate-500 ml-auto bg-slate-900 px-2 py-1 rounded">{getQueueName()}</span>
357
+ </h2>
358
+ <form onSubmit={handlePublish} className="flex gap-2 mb-4">
359
+ <input type="text" value={publishInput} onChange={(e) => setPublishInput(e.target.value)}
360
+ placeholder={connected ? "Message to publish..." : "Type to queue offline..."}
361
+ className="flex-1 px-4 py-2.5 rounded-lg bg-slate-800/50 border border-slate-700 focus:border-amber-500 focus:ring-1 focus:ring-amber-500 outline-none text-sm transition-colors placeholder:text-slate-600" />
362
+ <button type="submit" disabled={!publishInput.trim()}
363
+ className={`px-4 py-2.5 rounded-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed text-sm font-medium shadow-lg ${connected ? 'bg-amber-600 hover:bg-amber-500 shadow-amber-600/20' : 'bg-slate-700 hover:bg-slate-600 shadow-none text-slate-300'}`}>
364
+ {connected ? 'Publish' : `Queue (${outbox.length})`}
365
+ </button>
366
+ </form>
367
+ <div className="space-y-2 max-h-[400px] overflow-y-auto pr-2">
368
+ {publishedMessages.length === 0 && <p className="text-sm text-slate-500 text-center py-8">No messages published yet</p>}
369
+ {[...publishedMessages].reverse().map((msg) => (
370
+ <div key={`pub-${msg.id}-${msg.timestamp}`} className="flex items-center gap-3 p-3 rounded-lg bg-slate-900/50 border border-slate-700/50">
371
+ <Check className="w-3.5 h-3.5 text-green-500 shrink-0" />
372
+ <span className="text-sm flex-1 truncate font-medium text-slate-300">{msg.payload}</span>
373
+ <span className="text-xs text-slate-500 font-mono tracking-widest bg-slate-800 px-1.5 py-0.5 rounded">seq:{msg.id}</span>
374
+ </div>
375
+ ))}
376
+ </div>
377
+ </div>
378
+
379
+ {/* Consumer */}
380
+ <div className="glass rounded-2xl p-6 border-t border-slate-700/50">
381
+ <h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
382
+ <Download className="w-4 h-4 text-cyan-400" />Consumer <span className="text-xs font-mono text-slate-500 ml-auto bg-slate-900 px-2 py-1 rounded">{getQueueName()}</span>
383
+ </h2>
384
+ <div className="flex gap-2 mb-4">
385
+ <button onClick={handleConsume} disabled={!connected}
386
+ className="flex items-center gap-2 px-4 py-2.5 bg-cyan-600 hover:bg-cyan-500 rounded-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed text-sm font-medium shadow-lg shadow-cyan-600/20 w-1/3 justify-center">
387
+ <RefreshCw className="w-3.5 h-3.5" />Pull 1
388
+ </button>
389
+ <button onClick={handleDrainAll} disabled={!connected}
390
+ className="flex items-center gap-2 px-4 py-2.5 bg-indigo-600 hover:bg-indigo-500 rounded-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed text-sm font-medium shadow-lg shadow-indigo-600/20 w-1/3 justify-center">
391
+ <Download className="w-3.5 h-3.5" />Drain All
392
+ </button>
393
+ <button onClick={() => setReceivedMessages([])}
394
+ className="flex items-center gap-2 px-4 py-2.5 rounded-lg border border-slate-700 hover:bg-slate-800/50 transition-all text-sm text-slate-400 w-1/3 justify-center">
395
+ <Trash2 className="w-3.5 h-3.5" />Clear UI
396
+ </button>
397
+ </div>
398
+ <div className="space-y-2 max-h-[400px] overflow-y-auto pr-2">
399
+ {receivedMessages.length === 0 && <p className="text-sm text-slate-500 text-center py-8 italic">Inbox zero. Click Pull or Drain.</p>}
400
+ {[...receivedMessages].reverse().map((msg) => (
401
+ <div key={`rec-${msg.id}-${msg.timestamp}`}
402
+ className={`flex items-center gap-3 p-3 rounded-lg border ${msg.acked ? "bg-green-500/5 border-green-500/20" : "bg-slate-900 border-indigo-500/30 shadow-inner shadow-indigo-500/5"}`}>
403
+ {msg.acked ? <Check className="w-4 h-4 text-green-400 shrink-0" /> : <Clock className="w-4 h-4 text-amber-400 shrink-0 animate-pulse" />}
404
+ <div className="flex-1 min-w-0">
405
+ <div className="text-sm font-medium text-white truncate">{msg.payload}</div>
406
+ <div className="text-[10px] text-slate-500 font-mono tracking-widest mt-0.5">SEQ:{msg.id} | {new Date(msg.timestamp).toLocaleTimeString()}</div>
407
+ </div>
408
+ {!msg.acked ? (
409
+ <button onClick={() => handleAck(msg.id)} disabled={!connected}
410
+ className="text-xs px-3 py-1.5 font-bold tracking-widest bg-green-600 hover:bg-green-500 rounded-md transition-all disabled:opacity-50">ACK</button>
411
+ ) : (
412
+ <span className="text-[10px] font-bold text-green-500 uppercase tracking-widest mr-2">Acked</span>
413
+ )}
414
+ </div>
415
+ ))}
416
+ </div>
417
+ </div>
418
+ </div>
419
+ </>
420
+ )}
421
+ </div>
422
+ );
423
+ }
@@ -0,0 +1,5 @@
1
+ import QueueDemoPage from "./page.client";
2
+
3
+ export default function Page() {
4
+ return <QueueDemoPage />;
5
+ }