@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,11 @@
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
5
+ VITE_FLUXYCHAT_MEMBER_JWT=eyJ...
6
+ VITE_FLUXYCHAT_ROOM_ID=demo
7
+ # Optional — room-peer agent on the timeline
8
+ # VITE_FLUXYCHAT_AGENT_ID=agt_...
9
+
10
+ # Option B — public guest room
11
+ # VITE_FLUXYCHAT_PUBLIC_ROOM_ID=your-public-room-id
@@ -0,0 +1,9 @@
1
+ # FluxyChat fleet panel
2
+
3
+ HTTP GPS ingest (`POST /fleet/gps`) fans out `server_event` `fleet.gps_update` on the room. Not MQTT.
4
+
5
+ ```bash
6
+ npx @fluxy-chat/create-fluxy-chat@latest my-fleet --example fleet-panel
7
+ cp .env.example .env
8
+ npm run dev
9
+ ```
@@ -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 fleet panel</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-fleet-panel",
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,83 @@
1
+ import { useState } from "react";
2
+ import { FluxyRealtimeProvider, useChat, useFluxyChat } from "@fluxy-chat/react";
3
+ import { useFluxySession, workerUrl } from "./session";
4
+
5
+ function FleetBoard({ roomId }: { roomId: string }) {
6
+ const { client } = useFluxyChat();
7
+ const [rows, setRows] = useState<string[]>([]);
8
+ const [vehicleId, setVehicleId] = useState<string | null>(null);
9
+ const { connected } = useChat({
10
+ roomId,
11
+ replay: "request",
12
+ onServerEvent: (ev) => {
13
+ if (ev.name !== "fleet.gps_update") return;
14
+ const d = ev.data;
15
+ setRows((prev) =>
16
+ [`${String(d.vehicleId)} ${String(d.lat)},${String(d.lng)}`, ...prev].slice(0, 40),
17
+ );
18
+ },
19
+ });
20
+
21
+ async function ensureVehicle() {
22
+ if (!client) return null;
23
+ if (vehicleId) return vehicleId;
24
+ const created = await client.createFleetVehicle({ name: "demo-van" });
25
+ setVehicleId(created.vehicle.id);
26
+ return created.vehicle.id;
27
+ }
28
+
29
+ async function pingGps() {
30
+ if (!client) return;
31
+ const id = await ensureVehicle();
32
+ if (!id) return;
33
+ await client.ingestGps({
34
+ vehicleId: id,
35
+ lat: 45.46 + Math.random() * 0.01,
36
+ lng: 9.18 + Math.random() * 0.01,
37
+ roomId,
38
+ });
39
+ }
40
+
41
+ const curl = `curl -X POST "$VITE_FLUXYCHAT_WORKER_URL/fleet/gps" \\
42
+ -H "Authorization: Bearer $JWT" \\
43
+ -H "Content-Type: application/json" \\
44
+ -d '{"vehicleId":"<id>","lat":45.46,"lng":9.18,"roomId":"${roomId}"}'`;
45
+
46
+ return (
47
+ <section className="panel">
48
+ <header className="chat-header">
49
+ <strong>Fleet · {roomId}</strong>
50
+ <span className="status">{connected ? "listening for fleet.gps_update" : "connecting"}</span>
51
+ </header>
52
+ <p className="hint">{curl}</p>
53
+ <ul className="readings">
54
+ {rows.length === 0 ? <li>No GPS yet.</li> : null}
55
+ {rows.map((row, i) => (
56
+ <li key={`${row}-${i}`}>{row}</li>
57
+ ))}
58
+ </ul>
59
+ <div className="composer">
60
+ <button type="button" className="primary" onClick={() => void pingGps()}>
61
+ Post sample GPS
62
+ </button>
63
+ </div>
64
+ </section>
65
+ );
66
+ }
67
+
68
+ export function App() {
69
+ const { session, loading, error } = useFluxySession();
70
+ if (!workerUrl) return <div className="shell">Set <code>VITE_FLUXYCHAT_WORKER_URL</code>.</div>;
71
+ if (loading) return <div className="shell">Starting…</div>;
72
+ if (error) return <div className="shell error">{error}</div>;
73
+ if (!session) return <div className="shell">Set a member JWT or public room id.</div>;
74
+
75
+ return (
76
+ <div className="shell">
77
+ <p className="mode-badge">{session.mode} · HTTP GPS ingest. Not MQTT.</p>
78
+ <FluxyRealtimeProvider workerUrl={session.workerUrl} authTokenProvider={session.token} userId={session.userId}>
79
+ <FleetBoard roomId={session.roomId} />
80
+ </FluxyRealtimeProvider>
81
+ </div>
82
+ );
83
+ }
@@ -0,0 +1,75 @@
1
+ :root {
2
+ font-family: system-ui, -apple-system, sans-serif;
3
+ line-height: 1.5;
4
+ color: #0f172a;
5
+ background: #f8fafc;
6
+ }
7
+ * { box-sizing: border-box; }
8
+ body { margin: 0; }
9
+ .shell { max-width: 720px; margin: 0 auto; padding: 1.5rem; }
10
+ .panel {
11
+ border: 1px solid #e2e8f0;
12
+ border-radius: 12px;
13
+ overflow: hidden;
14
+ background: #fff;
15
+ }
16
+ .chat-header {
17
+ display: flex;
18
+ justify-content: space-between;
19
+ padding: 0.75rem 1rem;
20
+ background: #0f172a;
21
+ color: #fff;
22
+ }
23
+ .status { font-size: 0.75rem; opacity: 0.9; }
24
+ .roster {
25
+ margin: 0;
26
+ padding: 0.5rem 1rem;
27
+ list-style: none;
28
+ font-size: 0.8rem;
29
+ color: #64748b;
30
+ border-bottom: 1px solid #e2e8f0;
31
+ }
32
+ .log { min-height: 240px; max-height: 50vh; overflow: auto; padding: 1rem; }
33
+ .composer { display: flex; gap: 0.5rem; padding: 0.75rem; border-top: 1px solid #e2e8f0; }
34
+ .composer input { flex: 1; padding: 0.5rem 0.75rem; border: 1px solid #cbd5e1; border-radius: 8px; }
35
+ .composer button, button.primary {
36
+ padding: 0.5rem 0.75rem;
37
+ border: 0;
38
+ border-radius: 8px;
39
+ background: #2563eb;
40
+ color: #fff;
41
+ cursor: pointer;
42
+ }
43
+ .mode-badge { font-size: 0.75rem; color: #64748b; margin-bottom: 0.75rem; }
44
+ .error { color: #b91c1c; }
45
+ code { font-size: 0.85em; }
46
+ .hint { font-size: 0.8rem; color: #475569; white-space: pre-wrap; background: #f1f5f9; padding: 0.75rem; border-radius: 8px; }
47
+ .readings { margin: 0; padding: 0.75rem 1rem; list-style: none; font-size: 0.875rem; }
48
+ .canvas {
49
+ position: relative;
50
+ height: min(70vh, 480px);
51
+ background: #f8fafc;
52
+ touch-action: none;
53
+ }
54
+ .dot {
55
+ position: absolute;
56
+ width: 10px;
57
+ height: 10px;
58
+ border-radius: 999px;
59
+ transform: translate(-50%, -50%);
60
+ pointer-events: none;
61
+ }
62
+ .peer-cursor {
63
+ position: absolute;
64
+ pointer-events: none;
65
+ z-index: 2;
66
+ }
67
+ .peer-cursor span {
68
+ display: block;
69
+ margin-top: 2px;
70
+ padding: 1px 6px;
71
+ border-radius: 4px;
72
+ background: currentColor;
73
+ color: #fff;
74
+ font-size: 11px;
75
+ }
@@ -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,73 @@
1
+ import { useEffect, useState } from "react";
2
+ import { FluxyChatClient } from "@fluxy-chat/sdk";
3
+
4
+ const workerUrl = import.meta.env.VITE_FLUXYCHAT_WORKER_URL?.trim();
5
+ const memberJwt = import.meta.env.VITE_FLUXYCHAT_MEMBER_JWT?.trim();
6
+ const publicRoomId = import.meta.env.VITE_FLUXYCHAT_PUBLIC_ROOM_ID?.trim();
7
+ const configuredRoomId = import.meta.env.VITE_FLUXYCHAT_ROOM_ID?.trim() || "demo";
8
+
9
+ export interface FluxySession {
10
+ workerUrl: string;
11
+ token: string;
12
+ userId: string;
13
+ roomId: string;
14
+ mode: "member" | "guest";
15
+ }
16
+
17
+ export function useFluxySession(): {
18
+ session: FluxySession | null;
19
+ loading: boolean;
20
+ error: string | null;
21
+ } {
22
+ const [session, setSession] = useState<FluxySession | null>(null);
23
+ const [loading, setLoading] = useState(Boolean(workerUrl));
24
+ const [error, setError] = useState<string | null>(null);
25
+
26
+ useEffect(() => {
27
+ if (!workerUrl) {
28
+ setLoading(false);
29
+ return;
30
+ }
31
+ if (memberJwt) {
32
+ setSession({
33
+ workerUrl,
34
+ token: memberJwt,
35
+ userId: "demo-user",
36
+ roomId: configuredRoomId,
37
+ mode: "member",
38
+ });
39
+ setLoading(false);
40
+ return;
41
+ }
42
+ if (publicRoomId) {
43
+ let cancelled = false;
44
+ void FluxyChatClient.joinPublicRoomAsGuest(workerUrl, publicRoomId, {
45
+ displayName: "Guest",
46
+ })
47
+ .then((guest) => {
48
+ if (cancelled) return;
49
+ setSession({
50
+ workerUrl,
51
+ token: guest.token,
52
+ userId: guest.userId,
53
+ roomId: guest.roomId,
54
+ mode: "guest",
55
+ });
56
+ })
57
+ .catch((err) => {
58
+ if (!cancelled) setError(err instanceof Error ? err.message : "Guest session failed");
59
+ })
60
+ .finally(() => {
61
+ if (!cancelled) setLoading(false);
62
+ });
63
+ return () => {
64
+ cancelled = true;
65
+ };
66
+ }
67
+ setLoading(false);
68
+ }, []);
69
+
70
+ return { session, loading, error };
71
+ }
72
+
73
+ export { workerUrl };
@@ -0,0 +1,13 @@
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
+ readonly VITE_FLUXYCHAT_AGENT_ID?: string;
9
+ }
10
+
11
+ interface ImportMeta {
12
+ readonly env: ImportMetaEnv;
13
+ }
@@ -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
+ });
@@ -16,8 +16,8 @@
16
16
  "postinstall": "node scripts/hoist-fluxy-sdk.mjs"
17
17
  },
18
18
  "dependencies": {
19
- "@fluxy-chat/react": "^0.1.3",
20
- "@fluxy-chat/sdk": "^0.6.3",
19
+ "@fluxy-chat/react": "^0.1.4",
20
+ "@fluxy-chat/sdk": "^0.6.4",
21
21
  "@fluxy-chat/ui": "^0.1.4",
22
22
  "class-variance-authority": "^0.7.1",
23
23
  "clsx": "^2.1.1",
@@ -40,7 +40,7 @@
40
40
  },
41
41
  "pnpm": {
42
42
  "overrides": {
43
- "@fluxy-chat/sdk": "^0.6.3"
43
+ "@fluxy-chat/sdk": "^0.6.4"
44
44
  }
45
45
  }
46
46
  }
@@ -0,0 +1,11 @@
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
5
+ VITE_FLUXYCHAT_MEMBER_JWT=eyJ...
6
+ VITE_FLUXYCHAT_ROOM_ID=demo
7
+ # Optional — room-peer agent on the timeline
8
+ # VITE_FLUXYCHAT_AGENT_ID=agt_...
9
+
10
+ # Option B — public guest room
11
+ # VITE_FLUXYCHAT_PUBLIC_ROOM_ID=your-public-room-id
@@ -0,0 +1,9 @@
1
+ # FluxyChat game tick
2
+
3
+ Lobby → match → `POST /games/matches/:id/input` fans out `server_event` `game.tick` on the same room WebSocket. **Not a netcode engine.**
4
+
5
+ ```bash
6
+ npx @fluxy-chat/create-fluxy-chat@latest my-game --example game-tick
7
+ cp .env.example .env
8
+ npm run dev
9
+ ```
@@ -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 game tick</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-game-tick",
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,87 @@
1
+ import { useMemo, useState } from "react";
2
+ import { createWorkerFluxyGameClient } from "@fluxy-chat/sdk";
3
+ import { FluxyRealtimeProvider, useChat, useFluxyChat } from "@fluxy-chat/react";
4
+ import { useFluxySession, workerUrl } from "./session";
5
+
6
+ function GameBoard({ roomId, userId }: { roomId: string; userId: string }) {
7
+ const { client } = useFluxyChat();
8
+ const game = useMemo(() => (client ? createWorkerFluxyGameClient(client) : null), [client]);
9
+ const [ticks, setTicks] = useState<string[]>([]);
10
+ const [matchId, setMatchId] = useState<string | null>(null);
11
+ const [error, setError] = useState<string | null>(null);
12
+ const { connected } = useChat({
13
+ roomId,
14
+ replay: "request",
15
+ onServerEvent: (ev) => {
16
+ if (ev.name === "game.tick" || ev.name === "game.match_started") {
17
+ setTicks((prev) => [`${ev.name} ${JSON.stringify(ev.data)}`, ...prev].slice(0, 40));
18
+ }
19
+ },
20
+ });
21
+
22
+ async function start() {
23
+ if (!game) return;
24
+ setError(null);
25
+ try {
26
+ const gameMode = `demo-${Date.now()}`;
27
+ await game.matchmake({ playerId: userId, roomId, gameMode, maxPlayers: 2 });
28
+ const lobby = await game.matchmake({
29
+ playerId: `${userId}-cpu`,
30
+ roomId,
31
+ gameMode,
32
+ maxPlayers: 2,
33
+ });
34
+ const match = await game.startMatch(lobby.lobbyId);
35
+ setMatchId(match.matchId);
36
+ } catch (err) {
37
+ setError(err instanceof Error ? err.message : "match_failed");
38
+ }
39
+ }
40
+
41
+ async function inputTick() {
42
+ if (!game || !matchId) return;
43
+ await game.submitInput(matchId, { playerId: userId, actions: { move: "nudge" } });
44
+ }
45
+
46
+ return (
47
+ <section className="panel">
48
+ <header className="chat-header">
49
+ <strong>Game tick · {roomId}</strong>
50
+ <span className="status">{connected ? "listening for game.tick" : "connecting"}</span>
51
+ </header>
52
+ <p className="hint">Not netcode. Matchmake binds this room, then input posts a server_event tick.</p>
53
+ {error ? <p className="error" style={{ padding: "0 1rem" }}>{error}</p> : null}
54
+ <ul className="readings">
55
+ {ticks.length === 0 ? <li>No ticks yet.</li> : null}
56
+ {ticks.map((row, i) => (
57
+ <li key={`${i}-${row.slice(0, 24)}`}>{row}</li>
58
+ ))}
59
+ </ul>
60
+ <div className="composer">
61
+ <button type="button" className="primary" onClick={() => void start()}>
62
+ Matchmake + start
63
+ </button>
64
+ <button type="button" className="primary" disabled={!matchId} onClick={() => void inputTick()}>
65
+ Submit input
66
+ </button>
67
+ </div>
68
+ </section>
69
+ );
70
+ }
71
+
72
+ export function App() {
73
+ const { session, loading, error } = useFluxySession();
74
+ if (!workerUrl) return <div className="shell">Set <code>VITE_FLUXYCHAT_WORKER_URL</code>.</div>;
75
+ if (loading) return <div className="shell">Starting…</div>;
76
+ if (error) return <div className="shell error">{error}</div>;
77
+ if (!session) return <div className="shell">Set a member JWT or public room id.</div>;
78
+
79
+ return (
80
+ <div className="shell">
81
+ <p className="mode-badge">{session.mode} · server_event ticks, not rollback netcode</p>
82
+ <FluxyRealtimeProvider workerUrl={session.workerUrl} authTokenProvider={session.token} userId={session.userId}>
83
+ <GameBoard roomId={session.roomId} userId={session.userId} />
84
+ </FluxyRealtimeProvider>
85
+ </div>
86
+ );
87
+ }
@@ -0,0 +1,75 @@
1
+ :root {
2
+ font-family: system-ui, -apple-system, sans-serif;
3
+ line-height: 1.5;
4
+ color: #0f172a;
5
+ background: #f8fafc;
6
+ }
7
+ * { box-sizing: border-box; }
8
+ body { margin: 0; }
9
+ .shell { max-width: 720px; margin: 0 auto; padding: 1.5rem; }
10
+ .panel {
11
+ border: 1px solid #e2e8f0;
12
+ border-radius: 12px;
13
+ overflow: hidden;
14
+ background: #fff;
15
+ }
16
+ .chat-header {
17
+ display: flex;
18
+ justify-content: space-between;
19
+ padding: 0.75rem 1rem;
20
+ background: #0f172a;
21
+ color: #fff;
22
+ }
23
+ .status { font-size: 0.75rem; opacity: 0.9; }
24
+ .roster {
25
+ margin: 0;
26
+ padding: 0.5rem 1rem;
27
+ list-style: none;
28
+ font-size: 0.8rem;
29
+ color: #64748b;
30
+ border-bottom: 1px solid #e2e8f0;
31
+ }
32
+ .log { min-height: 240px; max-height: 50vh; overflow: auto; padding: 1rem; }
33
+ .composer { display: flex; gap: 0.5rem; padding: 0.75rem; border-top: 1px solid #e2e8f0; }
34
+ .composer input { flex: 1; padding: 0.5rem 0.75rem; border: 1px solid #cbd5e1; border-radius: 8px; }
35
+ .composer button, button.primary {
36
+ padding: 0.5rem 0.75rem;
37
+ border: 0;
38
+ border-radius: 8px;
39
+ background: #2563eb;
40
+ color: #fff;
41
+ cursor: pointer;
42
+ }
43
+ .mode-badge { font-size: 0.75rem; color: #64748b; margin-bottom: 0.75rem; }
44
+ .error { color: #b91c1c; }
45
+ code { font-size: 0.85em; }
46
+ .hint { font-size: 0.8rem; color: #475569; white-space: pre-wrap; background: #f1f5f9; padding: 0.75rem; border-radius: 8px; }
47
+ .readings { margin: 0; padding: 0.75rem 1rem; list-style: none; font-size: 0.875rem; }
48
+ .canvas {
49
+ position: relative;
50
+ height: min(70vh, 480px);
51
+ background: #f8fafc;
52
+ touch-action: none;
53
+ }
54
+ .dot {
55
+ position: absolute;
56
+ width: 10px;
57
+ height: 10px;
58
+ border-radius: 999px;
59
+ transform: translate(-50%, -50%);
60
+ pointer-events: none;
61
+ }
62
+ .peer-cursor {
63
+ position: absolute;
64
+ pointer-events: none;
65
+ z-index: 2;
66
+ }
67
+ .peer-cursor span {
68
+ display: block;
69
+ margin-top: 2px;
70
+ padding: 1px 6px;
71
+ border-radius: 4px;
72
+ background: currentColor;
73
+ color: #fff;
74
+ font-size: 11px;
75
+ }
@@ -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,73 @@
1
+ import { useEffect, useState } from "react";
2
+ import { FluxyChatClient } from "@fluxy-chat/sdk";
3
+
4
+ const workerUrl = import.meta.env.VITE_FLUXYCHAT_WORKER_URL?.trim();
5
+ const memberJwt = import.meta.env.VITE_FLUXYCHAT_MEMBER_JWT?.trim();
6
+ const publicRoomId = import.meta.env.VITE_FLUXYCHAT_PUBLIC_ROOM_ID?.trim();
7
+ const configuredRoomId = import.meta.env.VITE_FLUXYCHAT_ROOM_ID?.trim() || "demo";
8
+
9
+ export interface FluxySession {
10
+ workerUrl: string;
11
+ token: string;
12
+ userId: string;
13
+ roomId: string;
14
+ mode: "member" | "guest";
15
+ }
16
+
17
+ export function useFluxySession(): {
18
+ session: FluxySession | null;
19
+ loading: boolean;
20
+ error: string | null;
21
+ } {
22
+ const [session, setSession] = useState<FluxySession | null>(null);
23
+ const [loading, setLoading] = useState(Boolean(workerUrl));
24
+ const [error, setError] = useState<string | null>(null);
25
+
26
+ useEffect(() => {
27
+ if (!workerUrl) {
28
+ setLoading(false);
29
+ return;
30
+ }
31
+ if (memberJwt) {
32
+ setSession({
33
+ workerUrl,
34
+ token: memberJwt,
35
+ userId: "demo-user",
36
+ roomId: configuredRoomId,
37
+ mode: "member",
38
+ });
39
+ setLoading(false);
40
+ return;
41
+ }
42
+ if (publicRoomId) {
43
+ let cancelled = false;
44
+ void FluxyChatClient.joinPublicRoomAsGuest(workerUrl, publicRoomId, {
45
+ displayName: "Guest",
46
+ })
47
+ .then((guest) => {
48
+ if (cancelled) return;
49
+ setSession({
50
+ workerUrl,
51
+ token: guest.token,
52
+ userId: guest.userId,
53
+ roomId: guest.roomId,
54
+ mode: "guest",
55
+ });
56
+ })
57
+ .catch((err) => {
58
+ if (!cancelled) setError(err instanceof Error ? err.message : "Guest session failed");
59
+ })
60
+ .finally(() => {
61
+ if (!cancelled) setLoading(false);
62
+ });
63
+ return () => {
64
+ cancelled = true;
65
+ };
66
+ }
67
+ setLoading(false);
68
+ }, []);
69
+
70
+ return { session, loading, error };
71
+ }
72
+
73
+ export { workerUrl };