@sansavision/create-pulse 0.4.1 → 0.4.3
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 +27 -3
- package/dist/index.js +3 -1
- package/package.json +1 -1
- package/templates/nextjs-auth-demo/.env.example +6 -0
- package/templates/nextjs-auth-demo/README.md +125 -0
- package/templates/nextjs-auth-demo/_gitignore +33 -0
- package/templates/nextjs-auth-demo/drizzle.config.ts +10 -0
- package/templates/nextjs-auth-demo/eslint.config.mjs +18 -0
- package/templates/nextjs-auth-demo/next-env.d.ts +6 -0
- package/templates/nextjs-auth-demo/next.config.ts +7 -0
- package/templates/nextjs-auth-demo/package.json +36 -0
- package/templates/nextjs-auth-demo/postcss.config.mjs +7 -0
- package/templates/nextjs-auth-demo/public/file.svg +1 -0
- package/templates/nextjs-auth-demo/public/globe.svg +1 -0
- package/templates/nextjs-auth-demo/public/next.svg +1 -0
- package/templates/nextjs-auth-demo/public/vercel.svg +1 -0
- package/templates/nextjs-auth-demo/public/window.svg +1 -0
- package/templates/nextjs-auth-demo/src/app/api/auth/[...all]/route.ts +4 -0
- package/templates/nextjs-auth-demo/src/app/api/pulse/verify/route.ts +54 -0
- package/templates/nextjs-auth-demo/src/app/auth/sign-in/page.tsx +131 -0
- package/templates/nextjs-auth-demo/src/app/auth/sign-up/page.tsx +153 -0
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/chat/page.tsx +248 -0
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/encrypted-chat/page.tsx +198 -0
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/game-sync/page.tsx +192 -0
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/queues/page.tsx +297 -0
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/watch-together/page.tsx +258 -0
- package/templates/nextjs-auth-demo/src/app/dashboard/layout.tsx +109 -0
- package/templates/nextjs-auth-demo/src/app/dashboard/page.tsx +147 -0
- package/templates/nextjs-auth-demo/src/app/favicon.ico +0 -0
- package/templates/nextjs-auth-demo/src/app/globals.css +96 -0
- package/templates/nextjs-auth-demo/src/app/layout.tsx +27 -0
- package/templates/nextjs-auth-demo/src/app/page.tsx +254 -0
- package/templates/nextjs-auth-demo/src/lib/auth-client.ts +15 -0
- package/templates/nextjs-auth-demo/src/lib/auth.ts +14 -0
- package/templates/nextjs-auth-demo/src/lib/db.ts +6 -0
- package/templates/nextjs-auth-demo/src/lib/pulse.ts +45 -0
- package/templates/nextjs-auth-demo/src/lib/schema.ts +107 -0
- package/templates/nextjs-auth-demo/tsconfig.json +34 -0
- package/templates/react-queue-demo/README.md +6 -7
- package/templates/react-queue-demo/src/App.tsx +34 -13
- package/src/index.ts +0 -115
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useRef } from "react";
|
|
4
|
+
import { useSession } from "@/lib/auth-client";
|
|
5
|
+
import { connectWithAuth } from "@/lib/pulse";
|
|
6
|
+
import {
|
|
7
|
+
Video,
|
|
8
|
+
Play,
|
|
9
|
+
Pause,
|
|
10
|
+
SkipForward,
|
|
11
|
+
Users,
|
|
12
|
+
Shield,
|
|
13
|
+
Wifi,
|
|
14
|
+
WifiOff,
|
|
15
|
+
Loader2,
|
|
16
|
+
} from "lucide-react";
|
|
17
|
+
import type { PulseConnection } from "@sansavision/pulse-sdk";
|
|
18
|
+
|
|
19
|
+
export default function WatchTogetherPage() {
|
|
20
|
+
const { data: session } = useSession();
|
|
21
|
+
const [connected, setConnected] = useState(false);
|
|
22
|
+
const [authUser, setAuthUser] = useState<{ id: string; claims: Record<string, string> } | null>(null);
|
|
23
|
+
const [connecting, setConnecting] = useState(true);
|
|
24
|
+
const [playing, setPlaying] = useState(false);
|
|
25
|
+
const [currentTime, setCurrentTime] = useState(0);
|
|
26
|
+
const [viewers, setViewers] = useState<string[]>([]);
|
|
27
|
+
const [syncEvents, setSyncEvents] = useState<string[]>([]);
|
|
28
|
+
const connRef = useRef<PulseConnection | null>(null);
|
|
29
|
+
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (!session) return;
|
|
33
|
+
let cancelled = false;
|
|
34
|
+
|
|
35
|
+
async function init() {
|
|
36
|
+
try {
|
|
37
|
+
const connection = await connectWithAuth();
|
|
38
|
+
if (cancelled) return;
|
|
39
|
+
|
|
40
|
+
connRef.current = connection;
|
|
41
|
+
setConnected(true);
|
|
42
|
+
setConnecting(false);
|
|
43
|
+
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
45
|
+
const user = (connection as any).user;
|
|
46
|
+
if (user) setAuthUser(user);
|
|
47
|
+
|
|
48
|
+
const stream = connection.stream("watch-room");
|
|
49
|
+
|
|
50
|
+
stream.send(
|
|
51
|
+
JSON.stringify({ type: "viewer-join", name: session!.user.name })
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
stream.on("data", (data: Uint8Array) => {
|
|
55
|
+
try {
|
|
56
|
+
const msg = JSON.parse(new TextDecoder().decode(data));
|
|
57
|
+
if (msg.type === "play") {
|
|
58
|
+
setPlaying(true);
|
|
59
|
+
setSyncEvents((prev) => [
|
|
60
|
+
`${msg.user} pressed play at ${msg.time?.toFixed(1)}s`,
|
|
61
|
+
...prev.slice(0, 9),
|
|
62
|
+
]);
|
|
63
|
+
} else if (msg.type === "pause") {
|
|
64
|
+
setPlaying(false);
|
|
65
|
+
setSyncEvents((prev) => [
|
|
66
|
+
`${msg.user} paused at ${msg.time?.toFixed(1)}s`,
|
|
67
|
+
...prev.slice(0, 9),
|
|
68
|
+
]);
|
|
69
|
+
} else if (msg.type === "seek") {
|
|
70
|
+
setCurrentTime(msg.time || 0);
|
|
71
|
+
setSyncEvents((prev) => [
|
|
72
|
+
`${msg.user} seeked to ${msg.time?.toFixed(1)}s`,
|
|
73
|
+
...prev.slice(0, 9),
|
|
74
|
+
]);
|
|
75
|
+
} else if (msg.type === "viewer-join") {
|
|
76
|
+
setViewers((prev) =>
|
|
77
|
+
prev.includes(msg.name) ? prev : [...prev, msg.name]
|
|
78
|
+
);
|
|
79
|
+
setSyncEvents((prev) => [
|
|
80
|
+
`${msg.name} joined the watch party`,
|
|
81
|
+
...prev.slice(0, 9),
|
|
82
|
+
]);
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
/* ignore */
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
connection.on("disconnect", () => setConnected(false));
|
|
90
|
+
connection.on("reconnected", () => setConnected(true));
|
|
91
|
+
} catch (err) {
|
|
92
|
+
console.error("Failed to connect:", err);
|
|
93
|
+
setConnecting(false);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
init();
|
|
98
|
+
return () => {
|
|
99
|
+
cancelled = true;
|
|
100
|
+
connRef.current?.disconnect();
|
|
101
|
+
if (intervalRef.current) clearInterval(intervalRef.current);
|
|
102
|
+
};
|
|
103
|
+
}, [session]);
|
|
104
|
+
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (playing) {
|
|
107
|
+
intervalRef.current = setInterval(
|
|
108
|
+
() => setCurrentTime((t) => t + 0.1),
|
|
109
|
+
100
|
|
110
|
+
);
|
|
111
|
+
} else {
|
|
112
|
+
if (intervalRef.current) clearInterval(intervalRef.current);
|
|
113
|
+
}
|
|
114
|
+
return () => {
|
|
115
|
+
if (intervalRef.current) clearInterval(intervalRef.current);
|
|
116
|
+
};
|
|
117
|
+
}, [playing]);
|
|
118
|
+
|
|
119
|
+
function sendAction(type: string) {
|
|
120
|
+
if (!connRef.current) return;
|
|
121
|
+
const stream = connRef.current.stream("watch-room");
|
|
122
|
+
stream.send(
|
|
123
|
+
JSON.stringify({
|
|
124
|
+
type,
|
|
125
|
+
user: session?.user?.name,
|
|
126
|
+
time: currentTime,
|
|
127
|
+
})
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function handlePlayPause() {
|
|
132
|
+
const action = playing ? "pause" : "play";
|
|
133
|
+
sendAction(action);
|
|
134
|
+
setPlaying(!playing);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function handleSeek(seconds: number) {
|
|
138
|
+
setCurrentTime((prev) => Math.max(0, prev + seconds));
|
|
139
|
+
sendAction("seek");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const formatTime = (t: number) => {
|
|
143
|
+
const mins = Math.floor(t / 60);
|
|
144
|
+
const secs = Math.floor(t % 60);
|
|
145
|
+
return `${mins}:${secs.toString().padStart(2, "0")}`;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<div className="p-8">
|
|
150
|
+
<div className="flex items-center justify-between mb-8">
|
|
151
|
+
<div className="flex items-center gap-3">
|
|
152
|
+
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-pink-500 to-rose-600 flex items-center justify-center">
|
|
153
|
+
<Video className="w-6 h-6 text-white" />
|
|
154
|
+
</div>
|
|
155
|
+
<div>
|
|
156
|
+
<h1 className="text-2xl font-bold">Watch Together</h1>
|
|
157
|
+
<p className="text-sm text-slate-500">
|
|
158
|
+
Synchronized video playback across users
|
|
159
|
+
</p>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
<div className="flex items-center gap-3">
|
|
163
|
+
{authUser && (
|
|
164
|
+
<div className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-green-500/10 border border-green-500/20">
|
|
165
|
+
<Shield className="w-3.5 h-3.5 text-green-400" />
|
|
166
|
+
<span className="text-xs text-green-400">
|
|
167
|
+
{authUser.claims.name || authUser.id}
|
|
168
|
+
</span>
|
|
169
|
+
</div>
|
|
170
|
+
)}
|
|
171
|
+
{connected ? (
|
|
172
|
+
<div className="flex items-center gap-2">
|
|
173
|
+
<Wifi className="w-4 h-4 text-green-400" />
|
|
174
|
+
<span className="text-xs text-green-400">Connected</span>
|
|
175
|
+
</div>
|
|
176
|
+
) : (
|
|
177
|
+
<div className="flex items-center gap-2">
|
|
178
|
+
<WifiOff className="w-4 h-4 text-red-400" />
|
|
179
|
+
<span className="text-xs text-red-400">Disconnected</span>
|
|
180
|
+
</div>
|
|
181
|
+
)}
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
{connecting ? (
|
|
186
|
+
<div className="flex items-center justify-center py-20">
|
|
187
|
+
<Loader2 className="w-6 h-6 animate-spin text-purple-500 mr-2" />
|
|
188
|
+
<span className="text-slate-400">Connecting...</span>
|
|
189
|
+
</div>
|
|
190
|
+
) : (
|
|
191
|
+
<div className="grid lg:grid-cols-3 gap-8">
|
|
192
|
+
<div className="lg:col-span-2">
|
|
193
|
+
<div className="glass rounded-2xl overflow-hidden">
|
|
194
|
+
<div className="aspect-video bg-gradient-to-br from-slate-900 to-slate-800 flex items-center justify-center relative">
|
|
195
|
+
<div className="text-center">
|
|
196
|
+
<Video className="w-16 h-16 text-slate-600 mx-auto mb-4" />
|
|
197
|
+
<p className="text-slate-500 text-sm">Simulated Video Player</p>
|
|
198
|
+
<p className="text-3xl font-mono text-white mt-2">{formatTime(currentTime)}</p>
|
|
199
|
+
</div>
|
|
200
|
+
<div className="absolute bottom-0 left-0 right-0 h-1 bg-slate-700">
|
|
201
|
+
<div
|
|
202
|
+
className="h-full bg-gradient-to-r from-pink-500 to-rose-500 transition-all"
|
|
203
|
+
style={{ width: `${Math.min((currentTime / 300) * 100, 100)}%` }}
|
|
204
|
+
/>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
<div className="p-4 flex items-center gap-4">
|
|
209
|
+
<button onClick={handlePlayPause} disabled={!connected}
|
|
210
|
+
className="w-10 h-10 rounded-full bg-white text-black flex items-center justify-center hover:scale-105 transition-transform disabled:opacity-50">
|
|
211
|
+
{playing ? <Pause className="w-5 h-5" /> : <Play className="w-5 h-5 ml-0.5" />}
|
|
212
|
+
</button>
|
|
213
|
+
<button onClick={() => handleSeek(10)} disabled={!connected}
|
|
214
|
+
className="p-2 rounded-lg hover:bg-slate-800 transition-colors disabled:opacity-50">
|
|
215
|
+
<SkipForward className="w-4 h-4" />
|
|
216
|
+
</button>
|
|
217
|
+
<span className="text-sm text-slate-400 font-mono">{formatTime(currentTime)} / 5:00</span>
|
|
218
|
+
<div className="flex-1" />
|
|
219
|
+
<div className="flex items-center gap-2">
|
|
220
|
+
<Users className="w-4 h-4 text-slate-400" />
|
|
221
|
+
<span className="text-sm text-slate-400">{Math.max(viewers.length, 1)} watching</span>
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
|
|
227
|
+
<div className="space-y-6">
|
|
228
|
+
<div className="glass rounded-2xl p-6">
|
|
229
|
+
<h3 className="text-sm font-semibold mb-3 text-pink-400">Viewers</h3>
|
|
230
|
+
<div className="space-y-2">
|
|
231
|
+
<div className="flex items-center gap-2">
|
|
232
|
+
<div className="w-6 h-6 rounded-full bg-gradient-to-br from-purple-500 to-cyan-500 flex items-center justify-center text-[10px] font-bold">
|
|
233
|
+
{session?.user?.name?.charAt(0) || "?"}
|
|
234
|
+
</div>
|
|
235
|
+
<span className="text-sm">{session?.user?.name} (you)</span>
|
|
236
|
+
</div>
|
|
237
|
+
{viewers.filter((v) => v !== session?.user?.name).map((v) => (
|
|
238
|
+
<div key={v} className="flex items-center gap-2">
|
|
239
|
+
<div className="w-6 h-6 rounded-full bg-gradient-to-br from-pink-500 to-rose-500 flex items-center justify-center text-[10px] font-bold">{v.charAt(0)}</div>
|
|
240
|
+
<span className="text-sm">{v}</span>
|
|
241
|
+
</div>
|
|
242
|
+
))}
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
|
|
246
|
+
<div className="glass rounded-2xl p-6">
|
|
247
|
+
<h3 className="text-sm font-semibold mb-3 text-cyan-400">Sync Events</h3>
|
|
248
|
+
<div className="space-y-2 max-h-60 overflow-y-auto">
|
|
249
|
+
{syncEvents.length === 0 && <p className="text-xs text-slate-500">No sync events yet. Press play!</p>}
|
|
250
|
+
{syncEvents.map((event, i) => <div key={i} className="text-xs text-slate-400">{event}</div>)}
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
)}
|
|
256
|
+
</div>
|
|
257
|
+
);
|
|
258
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useRouter } from "next/navigation";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import { useSession, signOut } from "@/lib/auth-client";
|
|
6
|
+
import {
|
|
7
|
+
Zap,
|
|
8
|
+
LayoutDashboard,
|
|
9
|
+
MessageSquare,
|
|
10
|
+
Video,
|
|
11
|
+
Database,
|
|
12
|
+
Gamepad2,
|
|
13
|
+
Lock,
|
|
14
|
+
LogOut,
|
|
15
|
+
Loader2,
|
|
16
|
+
ChevronRight,
|
|
17
|
+
} from "lucide-react";
|
|
18
|
+
import { useEffect } from "react";
|
|
19
|
+
|
|
20
|
+
const navItems = [
|
|
21
|
+
{ href: "/dashboard", label: "Overview", icon: LayoutDashboard },
|
|
22
|
+
{ href: "/dashboard/demos/chat", label: "Real-time Chat", icon: MessageSquare },
|
|
23
|
+
{ href: "/dashboard/demos/watch-together", label: "Watch Together", icon: Video },
|
|
24
|
+
{ href: "/dashboard/demos/queues", label: "Durable Queues", icon: Database },
|
|
25
|
+
{ href: "/dashboard/demos/game-sync", label: "Game Sync", icon: Gamepad2 },
|
|
26
|
+
{ href: "/dashboard/demos/encrypted-chat", label: "E2E Encrypted", icon: Lock },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
export default function DashboardLayout({
|
|
30
|
+
children,
|
|
31
|
+
}: {
|
|
32
|
+
children: React.ReactNode;
|
|
33
|
+
}) {
|
|
34
|
+
const { data: session, isPending } = useSession();
|
|
35
|
+
const router = useRouter();
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (!isPending && !session) {
|
|
39
|
+
router.push("/auth/sign-in");
|
|
40
|
+
}
|
|
41
|
+
}, [session, isPending, router]);
|
|
42
|
+
|
|
43
|
+
if (isPending) {
|
|
44
|
+
return (
|
|
45
|
+
<div className="min-h-screen flex items-center justify-center">
|
|
46
|
+
<Loader2 className="w-8 h-8 animate-spin text-purple-500" />
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!session) return null;
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className="min-h-screen flex">
|
|
55
|
+
{/* Sidebar */}
|
|
56
|
+
<aside className="w-64 border-r border-slate-800 flex flex-col">
|
|
57
|
+
<div className="p-5 border-b border-slate-800">
|
|
58
|
+
<Link href="/" className="flex items-center gap-2">
|
|
59
|
+
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-purple-500 to-cyan-500 flex items-center justify-center">
|
|
60
|
+
<Zap className="w-5 h-5 text-white" />
|
|
61
|
+
</div>
|
|
62
|
+
<span className="text-lg font-bold">Pulse</span>
|
|
63
|
+
</Link>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<nav className="flex-1 p-3 space-y-1">
|
|
67
|
+
{navItems.map((item) => (
|
|
68
|
+
<Link
|
|
69
|
+
key={item.href}
|
|
70
|
+
href={item.href}
|
|
71
|
+
className="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-slate-400 hover:text-white hover:bg-slate-800/50 transition-all group"
|
|
72
|
+
>
|
|
73
|
+
<item.icon className="w-4 h-4" />
|
|
74
|
+
<span className="flex-1">{item.label}</span>
|
|
75
|
+
<ChevronRight className="w-3.5 h-3.5 opacity-0 group-hover:opacity-100 transition-opacity" />
|
|
76
|
+
</Link>
|
|
77
|
+
))}
|
|
78
|
+
</nav>
|
|
79
|
+
|
|
80
|
+
{/* User info */}
|
|
81
|
+
<div className="p-4 border-t border-slate-800">
|
|
82
|
+
<div className="flex items-center gap-3 mb-3">
|
|
83
|
+
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-purple-500 to-cyan-500 flex items-center justify-center text-xs font-bold">
|
|
84
|
+
{session.user.name?.charAt(0)?.toUpperCase() || "?"}
|
|
85
|
+
</div>
|
|
86
|
+
<div className="flex-1 min-w-0">
|
|
87
|
+
<div className="text-sm font-medium truncate">
|
|
88
|
+
{session.user.name}
|
|
89
|
+
</div>
|
|
90
|
+
<div className="text-xs text-slate-500 truncate">
|
|
91
|
+
{session.user.email}
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
<button
|
|
96
|
+
onClick={() => signOut().then(() => router.push("/"))}
|
|
97
|
+
className="flex items-center gap-2 w-full px-3 py-2 rounded-lg text-sm text-slate-400 hover:text-red-400 hover:bg-red-500/10 transition-all"
|
|
98
|
+
>
|
|
99
|
+
<LogOut className="w-4 h-4" />
|
|
100
|
+
Sign out
|
|
101
|
+
</button>
|
|
102
|
+
</div>
|
|
103
|
+
</aside>
|
|
104
|
+
|
|
105
|
+
{/* Main content */}
|
|
106
|
+
<main className="flex-1 overflow-y-auto">{children}</main>
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { useSession } from "@/lib/auth-client";
|
|
5
|
+
import {
|
|
6
|
+
MessageSquare,
|
|
7
|
+
Video,
|
|
8
|
+
Database,
|
|
9
|
+
Gamepad2,
|
|
10
|
+
Lock,
|
|
11
|
+
ArrowRight,
|
|
12
|
+
Shield,
|
|
13
|
+
Wifi,
|
|
14
|
+
Activity,
|
|
15
|
+
} from "lucide-react";
|
|
16
|
+
|
|
17
|
+
const demos = [
|
|
18
|
+
{
|
|
19
|
+
href: "/dashboard/demos/chat",
|
|
20
|
+
icon: MessageSquare,
|
|
21
|
+
title: "Real-time Chat",
|
|
22
|
+
desc: "Multi-user rooms with presence, typing indicators, and auth identity. Open a second tab with a different user to see live messaging.",
|
|
23
|
+
color: "from-blue-500 to-blue-600",
|
|
24
|
+
badge: "Pub/Sub",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
href: "/dashboard/demos/watch-together",
|
|
28
|
+
icon: Video,
|
|
29
|
+
title: "Watch Together",
|
|
30
|
+
desc: "Synchronized video playback. Play, pause, and seek in perfect sync across all connected users.",
|
|
31
|
+
color: "from-pink-500 to-rose-600",
|
|
32
|
+
badge: "Broadcast",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
href: "/dashboard/demos/queues",
|
|
36
|
+
icon: Database,
|
|
37
|
+
title: "Durable Queues",
|
|
38
|
+
desc: "Publish messages, simulate going offline, come back online — messages persist and are delivered. Full ACK/NACK flow.",
|
|
39
|
+
color: "from-amber-500 to-orange-600",
|
|
40
|
+
badge: "Queue",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
href: "/dashboard/demos/game-sync",
|
|
44
|
+
icon: Gamepad2,
|
|
45
|
+
title: "Game State Sync",
|
|
46
|
+
desc: "Shared game board with real-time state sync. Multiple users can interact with the same game state simultaneously.",
|
|
47
|
+
color: "from-green-500 to-emerald-600",
|
|
48
|
+
badge: "State",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
href: "/dashboard/demos/encrypted-chat",
|
|
52
|
+
icon: Lock,
|
|
53
|
+
title: "E2E Encrypted Chat",
|
|
54
|
+
desc: "End-to-end encrypted messaging where the relay cannot read content. Key exchange visualization included.",
|
|
55
|
+
color: "from-purple-500 to-violet-600",
|
|
56
|
+
badge: "P2P",
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
export default function DashboardPage() {
|
|
61
|
+
const { data: session } = useSession();
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div className="p-8">
|
|
65
|
+
{/* Header */}
|
|
66
|
+
<div className="mb-8">
|
|
67
|
+
<h1 className="text-3xl font-bold mb-2">
|
|
68
|
+
Welcome, {session?.user?.name?.split(" ")[0] || "there"} 👋
|
|
69
|
+
</h1>
|
|
70
|
+
<p className="text-slate-400 text-lg">
|
|
71
|
+
Choose a demo to explore Pulse's capabilities with authenticated
|
|
72
|
+
connections.
|
|
73
|
+
</p>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
{/* Connection status */}
|
|
77
|
+
<div className="glass rounded-xl p-5 mb-8 flex items-center gap-6">
|
|
78
|
+
<div className="flex items-center gap-2">
|
|
79
|
+
<Shield className="w-4 h-4 text-green-400" />
|
|
80
|
+
<span className="text-sm text-green-400 font-medium">
|
|
81
|
+
Authenticated
|
|
82
|
+
</span>
|
|
83
|
+
</div>
|
|
84
|
+
<div className="flex items-center gap-2">
|
|
85
|
+
<Wifi className="w-4 h-4 text-cyan-400" />
|
|
86
|
+
<span className="text-sm text-slate-400">
|
|
87
|
+
Relay:{" "}
|
|
88
|
+
<code className="text-cyan-400 bg-cyan-500/10 px-1.5 py-0.5 rounded text-xs">
|
|
89
|
+
{process.env.NEXT_PUBLIC_PULSE_URL || "ws://localhost:4001"}
|
|
90
|
+
</code>
|
|
91
|
+
</span>
|
|
92
|
+
</div>
|
|
93
|
+
<div className="flex items-center gap-2">
|
|
94
|
+
<Activity className="w-4 h-4 text-purple-400" />
|
|
95
|
+
<span className="text-sm text-slate-400">
|
|
96
|
+
Auth Mode:{" "}
|
|
97
|
+
<code className="text-purple-400 bg-purple-500/10 px-1.5 py-0.5 rounded text-xs">
|
|
98
|
+
webhook
|
|
99
|
+
</code>
|
|
100
|
+
</span>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
{/* Tip */}
|
|
105
|
+
<div className="glass rounded-xl p-5 mb-8 border-l-4 border-purple-500">
|
|
106
|
+
<h3 className="text-sm font-semibold text-purple-400 mb-1">
|
|
107
|
+
💡 Multi-User Tip
|
|
108
|
+
</h3>
|
|
109
|
+
<p className="text-sm text-slate-400">
|
|
110
|
+
Open a second browser tab (or incognito window), create a different
|
|
111
|
+
account, and interact simultaneously. Watch messages, video sync, and
|
|
112
|
+
game state flow in real-time between users.
|
|
113
|
+
</p>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
{/* Demo cards */}
|
|
117
|
+
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
118
|
+
{demos.map((demo) => (
|
|
119
|
+
<Link key={demo.href} href={demo.href}>
|
|
120
|
+
<div className="demo-card glass rounded-2xl p-6 h-full cursor-pointer group">
|
|
121
|
+
<div className="flex items-start justify-between mb-4">
|
|
122
|
+
<div
|
|
123
|
+
className={`w-12 h-12 rounded-xl bg-gradient-to-br ${demo.color} flex items-center justify-center`}
|
|
124
|
+
>
|
|
125
|
+
<demo.icon className="w-6 h-6 text-white" />
|
|
126
|
+
</div>
|
|
127
|
+
<span className="text-xs font-mono px-2 py-1 rounded-md bg-slate-800 text-slate-400 border border-slate-700">
|
|
128
|
+
{demo.badge}
|
|
129
|
+
</span>
|
|
130
|
+
</div>
|
|
131
|
+
<h3 className="text-lg font-semibold mb-2 group-hover:text-purple-400 transition-colors">
|
|
132
|
+
{demo.title}
|
|
133
|
+
</h3>
|
|
134
|
+
<p className="text-sm text-slate-400 leading-relaxed mb-4">
|
|
135
|
+
{demo.desc}
|
|
136
|
+
</p>
|
|
137
|
+
<div className="flex items-center gap-1 text-sm text-purple-400 font-medium">
|
|
138
|
+
Open Demo
|
|
139
|
+
<ArrowRight className="w-3.5 h-3.5 group-hover:translate-x-1 transition-transform" />
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
</Link>
|
|
143
|
+
))}
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
:root {
|
|
4
|
+
--color-primary: #7c3aed;
|
|
5
|
+
--color-primary-light: #a78bfa;
|
|
6
|
+
--color-accent: #06b6d4;
|
|
7
|
+
--color-surface: #0f172a;
|
|
8
|
+
--color-surface-elevated: #1e293b;
|
|
9
|
+
--color-surface-hover: #334155;
|
|
10
|
+
--color-text: #f1f5f9;
|
|
11
|
+
--color-text-muted: #94a3b8;
|
|
12
|
+
--color-border: #334155;
|
|
13
|
+
--color-danger: #ef4444;
|
|
14
|
+
--color-success: #10b981;
|
|
15
|
+
--color-warning: #f59e0b;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
* {
|
|
19
|
+
box-sizing: border-box;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
body {
|
|
23
|
+
font-family: "Inter", "SF Pro Display", -apple-system, system-ui, sans-serif;
|
|
24
|
+
background: var(--color-surface);
|
|
25
|
+
color: var(--color-text);
|
|
26
|
+
-webkit-font-smoothing: antialiased;
|
|
27
|
+
-moz-osx-font-smoothing: grayscale;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* Smooth scrollbar */
|
|
31
|
+
::-webkit-scrollbar {
|
|
32
|
+
width: 6px;
|
|
33
|
+
}
|
|
34
|
+
::-webkit-scrollbar-track {
|
|
35
|
+
background: transparent;
|
|
36
|
+
}
|
|
37
|
+
::-webkit-scrollbar-thumb {
|
|
38
|
+
background: var(--color-border);
|
|
39
|
+
border-radius: 3px;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* Glassmorphism utility */
|
|
43
|
+
.glass {
|
|
44
|
+
background: rgba(30, 41, 59, 0.7);
|
|
45
|
+
backdrop-filter: blur(16px);
|
|
46
|
+
-webkit-backdrop-filter: blur(16px);
|
|
47
|
+
border: 1px solid rgba(148, 163, 184, 0.1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* Gradient text */
|
|
51
|
+
.gradient-text {
|
|
52
|
+
background: linear-gradient(135deg, #7c3aed, #06b6d4, #10b981);
|
|
53
|
+
-webkit-background-clip: text;
|
|
54
|
+
-webkit-text-fill-color: transparent;
|
|
55
|
+
background-clip: text;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* Pulse animation */
|
|
59
|
+
@keyframes pulse-glow {
|
|
60
|
+
0%, 100% { box-shadow: 0 0 20px rgba(124, 58, 237, 0.3); }
|
|
61
|
+
50% { box-shadow: 0 0 40px rgba(124, 58, 237, 0.6); }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.pulse-glow {
|
|
65
|
+
animation: pulse-glow 2s ease-in-out infinite;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* Status indicator */
|
|
69
|
+
@keyframes status-blink {
|
|
70
|
+
0%, 100% { opacity: 1; }
|
|
71
|
+
50% { opacity: 0.5; }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.status-online {
|
|
75
|
+
width: 8px;
|
|
76
|
+
height: 8px;
|
|
77
|
+
border-radius: 50%;
|
|
78
|
+
background: var(--color-success);
|
|
79
|
+
animation: status-blink 2s ease-in-out infinite;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.status-offline {
|
|
83
|
+
width: 8px;
|
|
84
|
+
height: 8px;
|
|
85
|
+
border-radius: 50%;
|
|
86
|
+
background: var(--color-danger);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* Card hover effect */
|
|
90
|
+
.demo-card {
|
|
91
|
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
92
|
+
}
|
|
93
|
+
.demo-card:hover {
|
|
94
|
+
transform: translateY(-4px);
|
|
95
|
+
box-shadow: 0 20px 60px rgba(124, 58, 237, 0.15);
|
|
96
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import "./globals.css";
|
|
3
|
+
|
|
4
|
+
export const metadata: Metadata = {
|
|
5
|
+
title: "Pulse — Real-Time Protocol Demo",
|
|
6
|
+
description:
|
|
7
|
+
"Investor-ready demo showcasing PLP (Pulse Line Protocol) with auth, real-time chat, synchronized video, durable queues, and encrypted messaging.",
|
|
8
|
+
keywords: ["pulse", "real-time", "websocket", "protocol", "auth"],
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default function RootLayout({
|
|
12
|
+
children,
|
|
13
|
+
}: {
|
|
14
|
+
children: React.ReactNode;
|
|
15
|
+
}) {
|
|
16
|
+
return (
|
|
17
|
+
<html lang="en" className="dark">
|
|
18
|
+
<head>
|
|
19
|
+
<link
|
|
20
|
+
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap"
|
|
21
|
+
rel="stylesheet"
|
|
22
|
+
/>
|
|
23
|
+
</head>
|
|
24
|
+
<body className="min-h-screen antialiased">{children}</body>
|
|
25
|
+
</html>
|
|
26
|
+
);
|
|
27
|
+
}
|