@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
@@ -27,6 +27,7 @@ interface Participant {
27
27
  id: string;
28
28
  name: string;
29
29
  stream: MediaStream | null;
30
+ pc?: RTCPeerConnection;
30
31
  }
31
32
 
32
33
  export default function VideoCallPage() {
@@ -35,6 +36,7 @@ export default function VideoCallPage() {
35
36
  const [inCall, setInCall] = useState(false);
36
37
  const [roomId, setRoomId] = useState("pulse-room-" + Math.random().toString(36).slice(2, 8));
37
38
  const [joinRoomId, setJoinRoomId] = useState("");
39
+ const [joinError, setJoinError] = useState("");
38
40
  const [localStream, setLocalStream] = useState<MediaStream | null>(null);
39
41
  const [videoEnabled, setVideoEnabled] = useState(true);
40
42
  const [audioEnabled, setAudioEnabled] = useState(true);
@@ -143,12 +145,12 @@ export default function VideoCallPage() {
143
145
  const existing = prev.find((p) => p.id === remoteId);
144
146
  if (existing) {
145
147
  return prev.map((p) =>
146
- p.id === remoteId ? { ...p, stream: remoteStream } : p
148
+ p.id === remoteId ? { ...p, stream: remoteStream, pc } : p
147
149
  );
148
150
  }
149
151
  return [
150
152
  ...prev,
151
- { id: remoteId, name: remoteName, stream: remoteStream },
153
+ { id: remoteId, name: remoteName, stream: remoteStream, pc },
152
154
  ];
153
155
  });
154
156
  };
@@ -189,6 +191,12 @@ export default function VideoCallPage() {
189
191
  const joinCall = useCallback(
190
192
  async (room: string) => {
191
193
  if (!connRef.current || !session) return;
194
+ const validRoomRegex = /^pulse-room-[a-z0-9]{6}$/;
195
+ if (!validRoomRegex.test(room)) {
196
+ setJoinError("Invalid room ID format. Expected pulse-room-xxxxxx");
197
+ return;
198
+ }
199
+ setJoinError("");
192
200
 
193
201
  const stream = await getLocalMedia();
194
202
  if (!stream) return;
@@ -423,6 +431,7 @@ export default function VideoCallPage() {
423
431
  useEffect(() => {
424
432
  if (localVideoRef.current && localStream && inCall) {
425
433
  localVideoRef.current.srcObject = localStream;
434
+ localVideoRef.current.play().catch(() => { });
426
435
  }
427
436
  }, [localStream, inCall]);
428
437
 
@@ -544,8 +553,11 @@ export default function VideoCallPage() {
544
553
  <input
545
554
  type="text"
546
555
  value={joinRoomId}
547
- onChange={(e) => setJoinRoomId(e.target.value)}
548
- placeholder="Enter room ID..."
556
+ onChange={(e) => {
557
+ setJoinRoomId(e.target.value);
558
+ setJoinError("");
559
+ }}
560
+ placeholder="Enter room ID (pulse-room-xxxxxx)..."
549
561
  className="flex-1 px-4 py-2.5 rounded-xl bg-slate-800/50 border border-slate-700 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 outline-none text-sm transition-colors placeholder:text-slate-600"
550
562
  />
551
563
  <button
@@ -556,6 +568,7 @@ export default function VideoCallPage() {
556
568
  Join
557
569
  </button>
558
570
  </div>
571
+ {joinError && <p className="text-red-400 text-xs mt-2">{joinError}</p>}
559
572
  </div>
560
573
 
561
574
  {/* Info */}
@@ -576,7 +589,7 @@ export default function VideoCallPage() {
576
589
  <div className="h-full p-4 flex flex-col">
577
590
  <div className={`flex-1 grid ${gridCols} gap-3 auto-rows-fr`}>
578
591
  {/* Local Video */}
579
- <div className="relative rounded-2xl overflow-hidden bg-slate-900 border border-slate-800">
592
+ <div className="relative rounded-2xl overflow-hidden bg-slate-900 border border-slate-800 group">
580
593
  <video
581
594
  ref={localVideoRef}
582
595
  autoPlay
@@ -607,6 +620,19 @@ export default function VideoCallPage() {
607
620
  Sharing Screen
608
621
  </div>
609
622
  )}
623
+ <button
624
+ onClick={(e) => {
625
+ const target = e.currentTarget.parentElement;
626
+ if (!document.fullscreenElement) {
627
+ target?.requestFullscreen().catch(() => { });
628
+ } else {
629
+ document.exitFullscreen().catch(() => { });
630
+ }
631
+ }}
632
+ className="absolute top-3 right-3 p-1.5 rounded-lg bg-black/50 hover:bg-black/70 text-white backdrop-blur transition-colors opacity-0 group-hover:opacity-100"
633
+ >
634
+ <Maximize className="w-4 h-4" />
635
+ </button>
610
636
  </div>
611
637
 
612
638
  {/* Remote Participants */}
@@ -707,15 +733,76 @@ export default function VideoCallPage() {
707
733
  /* Remote video component */
708
734
  function RemoteVideo({ participant }: { participant: Participant }) {
709
735
  const videoRef = useRef<HTMLVideoElement>(null);
736
+ const [stats, setStats] = useState({ rtt: 0, bitrate: 0, packetLoss: 0 });
710
737
 
711
738
  useEffect(() => {
712
739
  if (videoRef.current && participant.stream) {
713
740
  videoRef.current.srcObject = participant.stream;
741
+ videoRef.current.play().catch(() => { });
714
742
  }
715
743
  }, [participant.stream]);
716
744
 
745
+ // Gather WebRTC stats with delta-based bitrate
746
+ useEffect(() => {
747
+ if (!participant.pc || !participant.stream) return;
748
+
749
+ let prevBytesReceived = 0;
750
+ let prevTimestamp = Date.now();
751
+
752
+ const interval = setInterval(async () => {
753
+ try {
754
+ const pc = participant.pc!;
755
+ const report = await pc.getStats();
756
+ let rtt = 0;
757
+ let lossPercent = 0;
758
+ let currentBytesReceived = 0;
759
+ let packetsLost = 0;
760
+ let packetsReceived = 0;
761
+
762
+ report.forEach((stat) => {
763
+ if (stat.type === "candidate-pair" && stat.state === "succeeded") {
764
+ if (stat.currentRoundTripTime !== undefined) rtt = Math.round(stat.currentRoundTripTime * 1000);
765
+ }
766
+ if (stat.type === "inbound-rtp" && stat.kind === "video") {
767
+ if (stat.bytesReceived !== undefined) currentBytesReceived = stat.bytesReceived;
768
+ if (stat.packetsLost !== undefined) packetsLost = stat.packetsLost;
769
+ if (stat.packetsReceived !== undefined) packetsReceived = stat.packetsReceived;
770
+ }
771
+ });
772
+
773
+ // Compute bitrate from byte delta
774
+ const now = Date.now();
775
+ const timeDeltaSecs = (now - prevTimestamp) / 1000;
776
+ let bitrateKbps = 0;
777
+ if (prevBytesReceived > 0 && timeDeltaSecs > 0) {
778
+ bitrateKbps = Math.round(((currentBytesReceived - prevBytesReceived) * 8) / timeDeltaSecs / 1000);
779
+ }
780
+ prevBytesReceived = currentBytesReceived;
781
+ prevTimestamp = now;
782
+
783
+ // Compute loss percentage
784
+ const totalPackets = packetsLost + packetsReceived;
785
+ if (totalPackets > 0) {
786
+ lossPercent = Math.round((packetsLost / totalPackets) * 10000) / 100;
787
+ }
788
+
789
+ setStats({ rtt, packetLoss: lossPercent, bitrate: bitrateKbps });
790
+ } catch { /* ignore */ }
791
+ }, 2000);
792
+
793
+ return () => clearInterval(interval);
794
+ }, [participant.pc, participant.stream]);
795
+
717
796
  return (
718
- <div className="relative rounded-2xl overflow-hidden bg-slate-900 border border-slate-800">
797
+ <div className="relative rounded-2xl overflow-hidden bg-slate-900 border border-slate-800 group">
798
+ {/* Stats Overlay - always visible when stream is active */}
799
+ {participant.stream && (
800
+ <div className="absolute top-3 left-3 flex flex-col gap-1 z-10 bg-black/60 p-2 rounded-lg backdrop-blur text-[10px] text-green-400 font-mono">
801
+ <div>RTT: {stats.rtt}ms</div>
802
+ <div>Loss: {stats.packetLoss}%</div>
803
+ <div>Bitrate: {stats.bitrate > 0 ? `${stats.bitrate} kbps` : "—"}</div>
804
+ </div>
805
+ )}
719
806
  {participant.stream ? (
720
807
  <video
721
808
  ref={videoRef}
@@ -735,6 +822,19 @@ function RemoteVideo({ participant }: { participant: Participant }) {
735
822
  {participant.name}
736
823
  </span>
737
824
  </div>
825
+ <button
826
+ onClick={(e) => {
827
+ const target = e.currentTarget.parentElement;
828
+ if (!document.fullscreenElement) {
829
+ target?.requestFullscreen().catch(() => { });
830
+ } else {
831
+ document.exitFullscreen().catch(() => { });
832
+ }
833
+ }}
834
+ className="absolute top-3 right-3 p-1.5 rounded-lg bg-black/50 hover:bg-black/70 text-white backdrop-blur transition-colors opacity-0 group-hover:opacity-100"
835
+ >
836
+ <Maximize className="w-4 h-4" />
837
+ </button>
738
838
  </div>
739
839
  );
740
840
  }