@fluxy-chat/create-fluxy-chat 0.5.11 → 0.5.13

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 (149) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/index.js +109 -0
  3. package/package.json +1 -1
  4. package/readme.md +11 -0
  5. package/templates/basic/package.json +1 -1
  6. package/templates/comments-board/.env.example +11 -0
  7. package/templates/comments-board/README.md +9 -0
  8. package/templates/comments-board/index.html +12 -0
  9. package/templates/comments-board/package.json +26 -0
  10. package/templates/comments-board/src/App.tsx +86 -0
  11. package/templates/comments-board/src/index.css +75 -0
  12. package/templates/comments-board/src/main.tsx +10 -0
  13. package/templates/comments-board/src/session.ts +73 -0
  14. package/templates/comments-board/src/vite-env.d.ts +13 -0
  15. package/templates/comments-board/tsconfig.json +21 -0
  16. package/templates/comments-board/vite.config.ts +7 -0
  17. package/templates/deal-room/.env.example +11 -0
  18. package/templates/deal-room/README.md +11 -0
  19. package/templates/deal-room/index.html +12 -0
  20. package/templates/deal-room/package.json +25 -0
  21. package/templates/deal-room/src/App.tsx +110 -0
  22. package/templates/deal-room/src/index.css +75 -0
  23. package/templates/deal-room/src/main.tsx +10 -0
  24. package/templates/deal-room/src/session.ts +73 -0
  25. package/templates/deal-room/src/vite-env.d.ts +13 -0
  26. package/templates/deal-room/tsconfig.json +21 -0
  27. package/templates/deal-room/vite.config.ts +7 -0
  28. package/templates/discord/package.json +1 -1
  29. package/templates/draw/.env.example +7 -0
  30. package/templates/draw/README.md +7 -0
  31. package/templates/draw/index.html +12 -0
  32. package/templates/draw/package.json +25 -0
  33. package/templates/draw/src/App.tsx +91 -0
  34. package/templates/draw/src/index.css +75 -0
  35. package/templates/draw/src/main.tsx +10 -0
  36. package/templates/draw/src/session.ts +73 -0
  37. package/templates/draw/src/vite-env.d.ts +13 -0
  38. package/templates/draw/tsconfig.json +21 -0
  39. package/templates/draw/vite.config.ts +7 -0
  40. package/templates/fleet-panel/.env.example +11 -0
  41. package/templates/fleet-panel/README.md +9 -0
  42. package/templates/fleet-panel/index.html +12 -0
  43. package/templates/fleet-panel/package.json +25 -0
  44. package/templates/fleet-panel/src/App.tsx +83 -0
  45. package/templates/fleet-panel/src/index.css +75 -0
  46. package/templates/fleet-panel/src/main.tsx +10 -0
  47. package/templates/fleet-panel/src/session.ts +73 -0
  48. package/templates/fleet-panel/src/vite-env.d.ts +13 -0
  49. package/templates/fleet-panel/tsconfig.json +21 -0
  50. package/templates/fleet-panel/vite.config.ts +7 -0
  51. package/templates/full/package.json +3 -3
  52. package/templates/game-tick/.env.example +11 -0
  53. package/templates/game-tick/README.md +9 -0
  54. package/templates/game-tick/index.html +12 -0
  55. package/templates/game-tick/package.json +25 -0
  56. package/templates/game-tick/src/App.tsx +87 -0
  57. package/templates/game-tick/src/index.css +75 -0
  58. package/templates/game-tick/src/main.tsx +10 -0
  59. package/templates/game-tick/src/session.ts +73 -0
  60. package/templates/game-tick/src/vite-env.d.ts +13 -0
  61. package/templates/game-tick/tsconfig.json +21 -0
  62. package/templates/game-tick/vite.config.ts +7 -0
  63. package/templates/iot-panel/.env.example +7 -0
  64. package/templates/iot-panel/README.md +16 -0
  65. package/templates/iot-panel/index.html +12 -0
  66. package/templates/iot-panel/package.json +25 -0
  67. package/templates/iot-panel/src/App.tsx +97 -0
  68. package/templates/iot-panel/src/index.css +75 -0
  69. package/templates/iot-panel/src/main.tsx +10 -0
  70. package/templates/iot-panel/src/session.ts +73 -0
  71. package/templates/iot-panel/src/vite-env.d.ts +13 -0
  72. package/templates/iot-panel/tsconfig.json +21 -0
  73. package/templates/iot-panel/vite.config.ts +7 -0
  74. package/templates/javascript-live-cursors/.env.example +9 -0
  75. package/templates/javascript-live-cursors/README.md +9 -0
  76. package/templates/javascript-live-cursors/index.html +17 -0
  77. package/templates/javascript-live-cursors/package.json +18 -0
  78. package/templates/javascript-live-cursors/src/index.css +31 -0
  79. package/templates/javascript-live-cursors/src/main.ts +89 -0
  80. package/templates/javascript-live-cursors/src/vite-env.d.ts +12 -0
  81. package/templates/javascript-live-cursors/tsconfig.json +13 -0
  82. package/templates/javascript-live-cursors/vite.config.ts +5 -0
  83. package/templates/live-cursors/.env.example +9 -0
  84. package/templates/live-cursors/README.md +20 -0
  85. package/templates/live-cursors/index.html +12 -0
  86. package/templates/live-cursors/package.json +25 -0
  87. package/templates/live-cursors/src/App.tsx +214 -0
  88. package/templates/live-cursors/src/index.css +94 -0
  89. package/templates/live-cursors/src/main.tsx +10 -0
  90. package/templates/live-cursors/src/vite-env.d.ts +12 -0
  91. package/templates/live-cursors/tsconfig.json +21 -0
  92. package/templates/live-cursors/vite.config.ts +7 -0
  93. package/templates/live-cursors-chat/.env.example +9 -0
  94. package/templates/live-cursors-chat/README.md +11 -0
  95. package/templates/live-cursors-chat/index.html +12 -0
  96. package/templates/live-cursors-chat/package.json +25 -0
  97. package/templates/live-cursors-chat/src/App.tsx +241 -0
  98. package/templates/live-cursors-chat/src/index.css +94 -0
  99. package/templates/live-cursors-chat/src/main.tsx +10 -0
  100. package/templates/live-cursors-chat/src/vite-env.d.ts +12 -0
  101. package/templates/live-cursors-chat/tsconfig.json +21 -0
  102. package/templates/live-cursors-chat/vite.config.ts +7 -0
  103. package/templates/minimal/package.json +2 -2
  104. package/templates/react/package.json +2 -2
  105. package/templates/slack/package.json +1 -1
  106. package/templates/telegram/package.json +1 -1
  107. package/templates/tiptap-room/.env.example +9 -0
  108. package/templates/tiptap-room/README.md +14 -0
  109. package/templates/tiptap-room/index.html +12 -0
  110. package/templates/tiptap-room/package.json +29 -0
  111. package/templates/tiptap-room/src/App.tsx +217 -0
  112. package/templates/tiptap-room/src/index.css +107 -0
  113. package/templates/tiptap-room/src/main.tsx +10 -0
  114. package/templates/tiptap-room/src/vite-env.d.ts +12 -0
  115. package/templates/tiptap-room/tsconfig.json +21 -0
  116. package/templates/tiptap-room/vite.config.ts +7 -0
  117. package/templates/voice-stage/.env.example +11 -0
  118. package/templates/voice-stage/README.md +11 -0
  119. package/templates/voice-stage/index.html +12 -0
  120. package/templates/voice-stage/package.json +25 -0
  121. package/templates/voice-stage/src/App.tsx +61 -0
  122. package/templates/voice-stage/src/index.css +75 -0
  123. package/templates/voice-stage/src/main.tsx +10 -0
  124. package/templates/voice-stage/src/session.ts +73 -0
  125. package/templates/voice-stage/src/vite-env.d.ts +13 -0
  126. package/templates/voice-stage/tsconfig.json +21 -0
  127. package/templates/voice-stage/vite.config.ts +7 -0
  128. package/templates/war-room/.env.example +11 -0
  129. package/templates/war-room/README.md +11 -0
  130. package/templates/war-room/index.html +12 -0
  131. package/templates/war-room/package.json +26 -0
  132. package/templates/war-room/src/App.tsx +120 -0
  133. package/templates/war-room/src/index.css +75 -0
  134. package/templates/war-room/src/main.tsx +10 -0
  135. package/templates/war-room/src/session.ts +73 -0
  136. package/templates/war-room/src/vite-env.d.ts +13 -0
  137. package/templates/war-room/tsconfig.json +21 -0
  138. package/templates/war-room/vite.config.ts +7 -0
  139. package/templates/whiteboard/.env.example +11 -0
  140. package/templates/whiteboard/README.md +9 -0
  141. package/templates/whiteboard/index.html +12 -0
  142. package/templates/whiteboard/package.json +26 -0
  143. package/templates/whiteboard/src/App.tsx +122 -0
  144. package/templates/whiteboard/src/index.css +75 -0
  145. package/templates/whiteboard/src/main.tsx +10 -0
  146. package/templates/whiteboard/src/session.ts +73 -0
  147. package/templates/whiteboard/src/vite-env.d.ts +13 -0
  148. package/templates/whiteboard/tsconfig.json +21 -0
  149. package/templates/whiteboard/vite.config.ts +7 -0
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "fluxy-live-cursors",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc -b && vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "@fluxy-chat/react": "^0.1.4",
13
+ "@fluxy-chat/sdk": "^0.6.4",
14
+ "react": "^19.0.0",
15
+ "react-dom": "^19.0.0",
16
+ "zustand": "^5.0.0"
17
+ },
18
+ "devDependencies": {
19
+ "@types/react": "^19.0.0",
20
+ "@types/react-dom": "^19.0.0",
21
+ "@vitejs/plugin-react": "^4.3.0",
22
+ "typescript": "^5.6.0",
23
+ "vite": "^6.0.0"
24
+ }
25
+ }
@@ -0,0 +1,214 @@
1
+ import { useEffect, useMemo, useState } from "react";
2
+ import { FluxyChatClient } from "@fluxy-chat/sdk";
3
+ import { FluxyRealtimeProvider, useChat } from "@fluxy-chat/react";
4
+
5
+ const workerUrl = import.meta.env.VITE_FLUXYCHAT_WORKER_URL?.trim();
6
+ const memberJwt = import.meta.env.VITE_FLUXYCHAT_MEMBER_JWT?.trim();
7
+ const publicRoomId = import.meta.env.VITE_FLUXYCHAT_PUBLIC_ROOM_ID?.trim();
8
+ const configuredRoomId = import.meta.env.VITE_FLUXYCHAT_ROOM_ID?.trim() || "demo";
9
+
10
+ const CURSOR_COLORS = ["#2563eb", "#db2777", "#059669", "#d97706", "#7c3aed"];
11
+
12
+ interface FluxySession {
13
+ workerUrl: string;
14
+ token: string;
15
+ userId: string;
16
+ roomId: string;
17
+ mode: "member" | "guest";
18
+ }
19
+
20
+ function useFluxySession(): {
21
+ session: FluxySession | null;
22
+ loading: boolean;
23
+ error: string | null;
24
+ } {
25
+ const [session, setSession] = useState<FluxySession | null>(null);
26
+ const [loading, setLoading] = useState(Boolean(workerUrl));
27
+ const [error, setError] = useState<string | null>(null);
28
+
29
+ useEffect(() => {
30
+ if (!workerUrl) {
31
+ setLoading(false);
32
+ return;
33
+ }
34
+
35
+ if (memberJwt) {
36
+ setSession({
37
+ workerUrl,
38
+ token: memberJwt,
39
+ userId: "demo-user",
40
+ roomId: configuredRoomId,
41
+ mode: "member",
42
+ });
43
+ setLoading(false);
44
+ return;
45
+ }
46
+
47
+ if (publicRoomId) {
48
+ let cancelled = false;
49
+ void FluxyChatClient.joinPublicRoomAsGuest(workerUrl, publicRoomId, {
50
+ displayName: "Guest",
51
+ })
52
+ .then((guest) => {
53
+ if (cancelled) return;
54
+ setSession({
55
+ workerUrl: workerUrl,
56
+ token: guest.token,
57
+ userId: guest.userId,
58
+ roomId: guest.roomId,
59
+ mode: "guest",
60
+ });
61
+ })
62
+ .catch((err) => {
63
+ if (!cancelled) {
64
+ setError(err instanceof Error ? err.message : "Guest session failed");
65
+ }
66
+ })
67
+ .finally(() => {
68
+ if (!cancelled) setLoading(false);
69
+ });
70
+ return () => {
71
+ cancelled = true;
72
+ };
73
+ }
74
+
75
+ setLoading(false);
76
+ }, []);
77
+
78
+ return { session, loading, error };
79
+ }
80
+
81
+ function CursorCanvas({
82
+ roomId,
83
+ selfUserId,
84
+ }: {
85
+ roomId: string;
86
+ selfUserId: string;
87
+ }) {
88
+ const { liveCursors, sendCursor, connected, connectionState } = useChat({
89
+ roomId,
90
+ replay: "request",
91
+ });
92
+ const color = CURSOR_COLORS[Math.abs(hashCode(selfUserId)) % CURSOR_COLORS.length];
93
+
94
+ return (
95
+ <section className="canvas-wrap">
96
+ <header className="chat-header">
97
+ <strong>{roomId}</strong>
98
+ <span className="status">
99
+ {connected ? "live" : connectionState.status} — open a second tab
100
+ </span>
101
+ </header>
102
+ <div
103
+ className="canvas"
104
+ onPointerMove={(event) => {
105
+ const rect = event.currentTarget.getBoundingClientRect();
106
+ sendCursor({
107
+ x: event.clientX - rect.left,
108
+ y: event.clientY - rect.top,
109
+ color,
110
+ label: selfUserId.slice(0, 12),
111
+ });
112
+ }}
113
+ onPointerLeave={() => {
114
+ /* last position stays until stale on peers */
115
+ }}
116
+ >
117
+ {Object.values(liveCursors)
118
+ .filter((c) => c.userId !== selfUserId)
119
+ .map((cursor) => (
120
+ <div
121
+ key={cursor.userId}
122
+ className="peer-cursor"
123
+ style={{
124
+ left: cursor.x,
125
+ top: cursor.y,
126
+ color: cursor.color || "#2563eb",
127
+ }}
128
+ >
129
+ <svg width="18" height="18" viewBox="0 0 24 24" aria-hidden>
130
+ <path
131
+ d="M5.5 3.2 19 12.4l-6.2 1.4 2.6 6.6-2.8 1.1-2.6-6.6L5.5 3.2Z"
132
+ fill="currentColor"
133
+ stroke="#0f172a"
134
+ strokeWidth="1"
135
+ />
136
+ </svg>
137
+ <span>{cursor.label || cursor.userId}</span>
138
+ </div>
139
+ ))}
140
+ </div>
141
+ </section>
142
+ );
143
+ }
144
+
145
+ function hashCode(value: string): number {
146
+ let hash = 0;
147
+ for (let i = 0; i < value.length; i += 1) hash = (hash * 31 + value.charCodeAt(i)) | 0;
148
+ return hash;
149
+ }
150
+
151
+ export function App() {
152
+ const { session, loading, error } = useFluxySession();
153
+ const [roomId, setRoomId] = useState(configuredRoomId);
154
+
155
+ const activeRoomId = useMemo(() => {
156
+ if (session?.mode === "guest") return session.roomId;
157
+ return roomId.trim() || configuredRoomId;
158
+ }, [session, roomId]);
159
+
160
+ if (!workerUrl) {
161
+ return (
162
+ <main className="shell">
163
+ <h1>FluxyChat — live cursors</h1>
164
+ <p>
165
+ Copy <code>.env.example</code> to <code>.env</code> and set{" "}
166
+ <code>VITE_FLUXYCHAT_WORKER_URL</code>.
167
+ </p>
168
+ <p>
169
+ Then either <code>VITE_FLUXYCHAT_MEMBER_JWT</code> or{" "}
170
+ <code>VITE_FLUXYCHAT_PUBLIC_ROOM_ID</code>.
171
+ </p>
172
+ </main>
173
+ );
174
+ }
175
+
176
+ if (loading) {
177
+ return (
178
+ <main className="shell">
179
+ <p>Connecting…</p>
180
+ </main>
181
+ );
182
+ }
183
+
184
+ if (error || !session) {
185
+ return (
186
+ <main className="shell">
187
+ <h1>Live cursors</h1>
188
+ <p className="error">{error ?? "Set JWT or public room ID in .env"}</p>
189
+ </main>
190
+ );
191
+ }
192
+
193
+ return (
194
+ <main className="shell">
195
+ <h1>Live cursors</h1>
196
+ <p className="mode-badge">
197
+ {session.mode === "guest" ? "Guest session" : "Member JWT"} — two tabs, same room
198
+ </p>
199
+ {session.mode === "member" ? (
200
+ <label className="room-picker">
201
+ Room
202
+ <input value={roomId} onChange={(e) => setRoomId(e.target.value)} />
203
+ </label>
204
+ ) : null}
205
+ <FluxyRealtimeProvider
206
+ workerUrl={session.workerUrl}
207
+ authTokenProvider={session.token}
208
+ userId={session.userId}
209
+ >
210
+ <CursorCanvas roomId={activeRoomId} selfUserId={session.userId} />
211
+ </FluxyRealtimeProvider>
212
+ </main>
213
+ );
214
+ }
@@ -0,0 +1,94 @@
1
+ :root {
2
+ font-family: system-ui, -apple-system, sans-serif;
3
+ line-height: 1.5;
4
+ color: #0f172a;
5
+ background: #f8fafc;
6
+ }
7
+
8
+ * {
9
+ box-sizing: border-box;
10
+ }
11
+
12
+ body {
13
+ margin: 0;
14
+ }
15
+
16
+ .shell {
17
+ max-width: 960px;
18
+ margin: 0 auto;
19
+ padding: 1.5rem;
20
+ }
21
+
22
+ .canvas-wrap {
23
+ border: 1px solid #e2e8f0;
24
+ border-radius: 12px;
25
+ overflow: hidden;
26
+ background: #fff;
27
+ }
28
+
29
+ .chat-header {
30
+ display: flex;
31
+ justify-content: space-between;
32
+ padding: 0.75rem 1rem;
33
+ border-bottom: 1px solid #e2e8f0;
34
+ background: #0f172a;
35
+ color: #fff;
36
+ }
37
+
38
+ .status {
39
+ font-size: 0.75rem;
40
+ opacity: 0.9;
41
+ }
42
+
43
+ .canvas {
44
+ position: relative;
45
+ height: min(70vh, 560px);
46
+ background:
47
+ linear-gradient(#e2e8f0 1px, transparent 1px) 0 0 / 32px 32px,
48
+ linear-gradient(90deg, #e2e8f0 1px, transparent 1px) 0 0 / 32px 32px,
49
+ #f8fafc;
50
+ cursor: none;
51
+ touch-action: none;
52
+ }
53
+
54
+ .peer-cursor {
55
+ position: absolute;
56
+ pointer-events: none;
57
+ transform: translate(-2px, -2px);
58
+ z-index: 2;
59
+ }
60
+
61
+ .peer-cursor span {
62
+ display: block;
63
+ margin-top: 2px;
64
+ padding: 1px 6px;
65
+ border-radius: 4px;
66
+ background: currentColor;
67
+ color: #fff;
68
+ font-size: 11px;
69
+ white-space: nowrap;
70
+ }
71
+
72
+ .mode-badge {
73
+ font-size: 0.75rem;
74
+ color: #64748b;
75
+ margin: 0 0 0.75rem;
76
+ }
77
+
78
+ .error {
79
+ color: #b91c1c;
80
+ }
81
+
82
+ .room-picker {
83
+ display: flex;
84
+ flex-direction: column;
85
+ gap: 0.25rem;
86
+ margin-bottom: 1rem;
87
+ font-size: 0.875rem;
88
+ }
89
+
90
+ .room-picker input {
91
+ padding: 0.5rem 0.75rem;
92
+ border: 1px solid #cbd5e1;
93
+ border-radius: 8px;
94
+ }
@@ -0,0 +1,10 @@
1
+ import { StrictMode } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import { App } from "./App";
4
+ import "./index.css";
5
+
6
+ createRoot(document.getElementById("root")!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ );
@@ -0,0 +1,12 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ interface ImportMetaEnv {
4
+ readonly VITE_FLUXYCHAT_WORKER_URL: string;
5
+ readonly VITE_FLUXYCHAT_MEMBER_JWT?: string;
6
+ readonly VITE_FLUXYCHAT_ROOM_ID?: string;
7
+ readonly VITE_FLUXYCHAT_PUBLIC_ROOM_ID?: string;
8
+ }
9
+
10
+ interface ImportMeta {
11
+ readonly env: ImportMetaEnv;
12
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+ "moduleResolution": "bundler",
9
+ "allowImportingTsExtensions": true,
10
+ "isolatedModules": true,
11
+ "moduleDetection": "force",
12
+ "noEmit": true,
13
+ "jsx": "react-jsx",
14
+ "strict": true,
15
+ "noUnusedLocals": true,
16
+ "noUnusedParameters": true,
17
+ "noFallthroughCasesInSwitch": true,
18
+ "noUncheckedSideEffectImports": true
19
+ },
20
+ "include": ["src"]
21
+ }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from "vite";
2
+ import react from "@vitejs/plugin-react";
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ server: { port: 5173 },
7
+ });
@@ -0,0 +1,9 @@
1
+ # FluxyChat worker URL (hosted or self-hosted)
2
+ VITE_FLUXYCHAT_WORKER_URL=https://your-worker.example.workers.dev
3
+
4
+ # Option A — member JWT (~2 min via onboarding)
5
+ VITE_FLUXYCHAT_MEMBER_JWT=eyJ...
6
+ VITE_FLUXYCHAT_ROOM_ID=demo
7
+
8
+ # Option B — public guest room (~60s, no signup)
9
+ # VITE_FLUXYCHAT_PUBLIC_ROOM_ID=your-public-room-id
@@ -0,0 +1,11 @@
1
+ # FluxyChat live cursors + chat
2
+
3
+ Same room WebSocket: `sendCursor` + `useChat` messages. Not `client_event` for pointers.
4
+
5
+ ```bash
6
+ npx @fluxy-chat/create-fluxy-chat@latest my-cursors-chat --example live-cursors-chat
7
+ cp .env.example .env
8
+ npm run dev
9
+ ```
10
+
11
+ Open two tabs. Move the pointer and send a chat line.
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>FluxyChat live cursors + chat</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "fluxy-live-cursors-chat",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc -b && vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "@fluxy-chat/react": "^0.1.4",
13
+ "@fluxy-chat/sdk": "^0.6.4",
14
+ "react": "^19.0.0",
15
+ "react-dom": "^19.0.0",
16
+ "zustand": "^5.0.0"
17
+ },
18
+ "devDependencies": {
19
+ "@types/react": "^19.0.0",
20
+ "@types/react-dom": "^19.0.0",
21
+ "@vitejs/plugin-react": "^4.3.0",
22
+ "typescript": "^5.6.0",
23
+ "vite": "^6.0.0"
24
+ }
25
+ }
@@ -0,0 +1,241 @@
1
+ import { useEffect, useMemo, useState } from "react";
2
+ import { FluxyChatClient } from "@fluxy-chat/sdk";
3
+ import { FluxyRealtimeProvider, useChat } from "@fluxy-chat/react";
4
+
5
+ const workerUrl = import.meta.env.VITE_FLUXYCHAT_WORKER_URL?.trim();
6
+ const memberJwt = import.meta.env.VITE_FLUXYCHAT_MEMBER_JWT?.trim();
7
+ const publicRoomId = import.meta.env.VITE_FLUXYCHAT_PUBLIC_ROOM_ID?.trim();
8
+ const configuredRoomId = import.meta.env.VITE_FLUXYCHAT_ROOM_ID?.trim() || "demo";
9
+
10
+ const CURSOR_COLORS = ["#2563eb", "#db2777", "#059669", "#d97706", "#7c3aed"];
11
+
12
+ interface FluxySession {
13
+ workerUrl: string;
14
+ token: string;
15
+ userId: string;
16
+ roomId: string;
17
+ mode: "member" | "guest";
18
+ }
19
+
20
+ function useFluxySession(): {
21
+ session: FluxySession | null;
22
+ loading: boolean;
23
+ error: string | null;
24
+ } {
25
+ const [session, setSession] = useState<FluxySession | null>(null);
26
+ const [loading, setLoading] = useState(Boolean(workerUrl));
27
+ const [error, setError] = useState<string | null>(null);
28
+
29
+ useEffect(() => {
30
+ if (!workerUrl) {
31
+ setLoading(false);
32
+ return;
33
+ }
34
+
35
+ if (memberJwt) {
36
+ setSession({
37
+ workerUrl,
38
+ token: memberJwt,
39
+ userId: "demo-user",
40
+ roomId: configuredRoomId,
41
+ mode: "member",
42
+ });
43
+ setLoading(false);
44
+ return;
45
+ }
46
+
47
+ if (publicRoomId) {
48
+ let cancelled = false;
49
+ void FluxyChatClient.joinPublicRoomAsGuest(workerUrl, publicRoomId, {
50
+ displayName: "Guest",
51
+ })
52
+ .then((guest) => {
53
+ if (cancelled) return;
54
+ setSession({
55
+ workerUrl: workerUrl,
56
+ token: guest.token,
57
+ userId: guest.userId,
58
+ roomId: guest.roomId,
59
+ mode: "guest",
60
+ });
61
+ })
62
+ .catch((err) => {
63
+ if (!cancelled) {
64
+ setError(err instanceof Error ? err.message : "Guest session failed");
65
+ }
66
+ })
67
+ .finally(() => {
68
+ if (!cancelled) setLoading(false);
69
+ });
70
+ return () => {
71
+ cancelled = true;
72
+ };
73
+ }
74
+
75
+ setLoading(false);
76
+ }, []);
77
+
78
+ return { session, loading, error };
79
+ }
80
+
81
+ function CursorCanvas({
82
+ roomId,
83
+ selfUserId,
84
+ }: {
85
+ roomId: string;
86
+ selfUserId: string;
87
+ }) {
88
+ const { liveCursors, sendCursor, connected, connectionState, messages, sendMessage } = useChat({
89
+ roomId,
90
+ replay: "request",
91
+ });
92
+ const [draft, setDraft] = useState("");
93
+ const color = CURSOR_COLORS[Math.abs(hashCode(selfUserId)) % CURSOR_COLORS.length];
94
+
95
+ return (
96
+ <section className="canvas-wrap">
97
+ <header className="chat-header">
98
+ <strong>{roomId}</strong>
99
+ <span className="status">
100
+ {connected ? "live" : connectionState.status} — open a second tab
101
+ </span>
102
+ </header>
103
+ <div
104
+ className="canvas"
105
+ onPointerMove={(event) => {
106
+ const rect = event.currentTarget.getBoundingClientRect();
107
+ sendCursor({
108
+ x: event.clientX - rect.left,
109
+ y: event.clientY - rect.top,
110
+ color,
111
+ label: selfUserId.slice(0, 12),
112
+ });
113
+ }}
114
+ onPointerLeave={() => {
115
+ /* last position stays until stale on peers */
116
+ }}
117
+ >
118
+ {Object.values(liveCursors)
119
+ .filter((c) => c.userId !== selfUserId)
120
+ .map((cursor) => (
121
+ <div
122
+ key={cursor.userId}
123
+ className="peer-cursor"
124
+ style={{
125
+ left: cursor.x,
126
+ top: cursor.y,
127
+ color: cursor.color || "#2563eb",
128
+ }}
129
+ >
130
+ <svg width="18" height="18" viewBox="0 0 24 24" aria-hidden>
131
+ <path
132
+ d="M5.5 3.2 19 12.4l-6.2 1.4 2.6 6.6-2.8 1.1-2.6-6.6L5.5 3.2Z"
133
+ fill="currentColor"
134
+ stroke="#0f172a"
135
+ strokeWidth="1"
136
+ />
137
+ </svg>
138
+ <span>{cursor.label || cursor.userId}</span>
139
+ </div>
140
+ ))}
141
+ </div>
142
+ <div className="log" style={{ maxHeight: 160, overflow: "auto", padding: "0.75rem 1rem" }}>
143
+ {messages.slice(-8).map((msg) => (
144
+ <p key={msg.id} style={{ margin: "0 0 6px" }}>
145
+ <strong>{msg.userId}:</strong> {msg.content}
146
+ </p>
147
+ ))}
148
+ </div>
149
+ <form
150
+ className="composer"
151
+ style={{ display: "flex", gap: 8, padding: "0.75rem", borderTop: "1px solid #e2e8f0" }}
152
+ onSubmit={(event) => {
153
+ event.preventDefault();
154
+ const text = draft.trim();
155
+ if (!text) return;
156
+ setDraft("");
157
+ void sendMessage(text);
158
+ }}
159
+ >
160
+ <input
161
+ value={draft}
162
+ onChange={(event) => setDraft(event.target.value)}
163
+ placeholder="Chat overlay…"
164
+ style={{ flex: 1, padding: "0.5rem", border: "1px solid #cbd5e1", borderRadius: 8 }}
165
+ />
166
+ <button type="submit">Send</button>
167
+ </form>
168
+ </section>
169
+ );
170
+ }
171
+
172
+ function hashCode(value: string): number {
173
+ let hash = 0;
174
+ for (let i = 0; i < value.length; i += 1) hash = (hash * 31 + value.charCodeAt(i)) | 0;
175
+ return hash;
176
+ }
177
+
178
+ export function App() {
179
+ const { session, loading, error } = useFluxySession();
180
+ const [roomId, setRoomId] = useState(configuredRoomId);
181
+
182
+ const activeRoomId = useMemo(() => {
183
+ if (session?.mode === "guest") return session.roomId;
184
+ return roomId.trim() || configuredRoomId;
185
+ }, [session, roomId]);
186
+
187
+ if (!workerUrl) {
188
+ return (
189
+ <main className="shell">
190
+ <h1>FluxyChat — live cursors + chat</h1>
191
+ <p>
192
+ Copy <code>.env.example</code> to <code>.env</code> and set{" "}
193
+ <code>VITE_FLUXYCHAT_WORKER_URL</code>.
194
+ </p>
195
+ <p>
196
+ Then either <code>VITE_FLUXYCHAT_MEMBER_JWT</code> or{" "}
197
+ <code>VITE_FLUXYCHAT_PUBLIC_ROOM_ID</code>.
198
+ </p>
199
+ </main>
200
+ );
201
+ }
202
+
203
+ if (loading) {
204
+ return (
205
+ <main className="shell">
206
+ <p>Connecting…</p>
207
+ </main>
208
+ );
209
+ }
210
+
211
+ if (error || !session) {
212
+ return (
213
+ <main className="shell">
214
+ <h1>Live cursors + chat</h1>
215
+ <p className="error">{error ?? "Set JWT or public room ID in .env"}</p>
216
+ </main>
217
+ );
218
+ }
219
+
220
+ return (
221
+ <main className="shell">
222
+ <h1>Live cursors + chat</h1>
223
+ <p className="mode-badge">
224
+ {session.mode === "guest" ? "Guest session" : "Member JWT"} — two tabs, same room
225
+ </p>
226
+ {session.mode === "member" ? (
227
+ <label className="room-picker">
228
+ Room
229
+ <input value={roomId} onChange={(e) => setRoomId(e.target.value)} />
230
+ </label>
231
+ ) : null}
232
+ <FluxyRealtimeProvider
233
+ workerUrl={session.workerUrl}
234
+ authTokenProvider={session.token}
235
+ userId={session.userId}
236
+ >
237
+ <CursorCanvas roomId={activeRoomId} selfUserId={session.userId} />
238
+ </FluxyRealtimeProvider>
239
+ </main>
240
+ );
241
+ }