create-bluecopa-react-app 1.0.11 → 1.0.12

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 (28) hide show
  1. package/bin/create-bluecopa-react-app.js +1 -1
  2. package/package.json +1 -1
  3. package/templates/latest/.dockerignore +5 -1
  4. package/templates/latest/Agent.md +577 -0
  5. package/templates/latest/Dockerfile +2 -2
  6. package/templates/latest/app/app.tsx +3 -1
  7. package/templates/latest/app/components/app-sidebar.tsx +14 -16
  8. package/templates/latest/app/components/nav-main.tsx +6 -22
  9. package/templates/latest/app/data/mock-payments.json +122 -0
  10. package/templates/latest/app/data/mock-transactions.json +128 -0
  11. package/templates/latest/app/routes/comments.tsx +552 -0
  12. package/templates/latest/app/routes/home.tsx +1 -1
  13. package/templates/latest/app/routes/payments.tsx +342 -0
  14. package/templates/latest/app/routes/websocket.tsx +449 -0
  15. package/templates/latest/app/routes.tsx +6 -0
  16. package/templates/latest/dist/assets/{__federation_expose_App-C8_sl1dD.js → __federation_expose_App-B2IoFaIA.js} +15 -4
  17. package/templates/latest/dist/assets/client-LFBsfOjG.js +2775 -0
  18. package/templates/latest/dist/assets/{home-DhyEFlEc.js → home-BBY02MnI.js} +87 -59
  19. package/templates/latest/dist/assets/{index-DkyIpbj3.js → index-CNNS7Foy.js} +4 -3
  20. package/templates/latest/dist/assets/{client-Hh38T4k9.js → index-D5og7-RT-BA7DwZw1.js} +46 -2789
  21. package/templates/latest/dist/assets/remoteEntry.css +4 -4
  22. package/templates/latest/dist/assets/remoteEntry.js +1 -1
  23. package/templates/latest/dist/index.html +3 -2
  24. package/templates/latest/package-lock.json +11 -11
  25. package/templates/latest/package.json +1 -1
  26. package/templates/latest/public/favicon.ico +0 -0
  27. package/templates/latest/public/avatars/shadcn.svg +0 -6
  28. /package/templates/latest/app/{dashboard → data}/data.json +0 -0
@@ -0,0 +1,449 @@
1
+ import { useState, useEffect, useRef } from "react";
2
+ import { AppSidebar } from "~/components/app-sidebar";
3
+ import { SiteHeader } from "~/components/site-header";
4
+ import { SidebarInset, SidebarProvider } from "~/components/ui/sidebar";
5
+ import { Button } from "~/components/ui/button";
6
+ import { Input } from "~/components/ui/input";
7
+ import { Label } from "~/components/ui/label";
8
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
9
+ import { Separator } from "~/components/ui/separator";
10
+ import { Badge } from "~/components/ui/badge";
11
+ import { copaUtils, copaSetConfig, copaGetConfig } from "@bluecopa/react";
12
+
13
+ interface WebSocketEvent {
14
+ id: string;
15
+ timestamp: Date;
16
+ channel: string;
17
+ event: string;
18
+ data: any;
19
+ }
20
+
21
+ export default function WebsocketTestPage() {
22
+ const [isConnected, setIsConnected] = useState(false);
23
+ const [connectionUrl, setConnectionUrl] = useState("");
24
+ const [channel, setChannel] = useState("test-channel");
25
+ const [eventName, setEventName] = useState("message");
26
+ const [events, setEvents] = useState<WebSocketEvent[]>([]);
27
+ const [subscriptions, setSubscriptions] = useState<Set<string>>(new Set());
28
+ const wsProviderRef = useRef<any | null>(null);
29
+
30
+ // Cleanup on unmount
31
+ useEffect(() => {
32
+ return () => {
33
+ if (wsProviderRef && wsProviderRef.current) {
34
+ wsProviderRef.current.disconnect();
35
+ }
36
+ };
37
+ }, []);
38
+
39
+ const handleConnect = () => {
40
+ try {
41
+ // Get current config to use access token and userId
42
+ const config = copaGetConfig();
43
+ console.log("config", config);
44
+
45
+ if (!config.accessToken || !config.userId) {
46
+ addEvent("system", "error", {
47
+ error: "Missing configuration. Ensure accessToken and userId are set via copaSetConfig()"
48
+ });
49
+ return;
50
+ }
51
+
52
+ // Create websocket provider using the factory
53
+ wsProviderRef.current = copaUtils.websocketUtils.WebsocketContextFactory.create(
54
+ "centrifugo",
55
+ {
56
+ connectionUrl: connectionUrl || undefined
57
+ }
58
+ );
59
+
60
+ setIsConnected(true);
61
+ addEvent("system", "connected", { url: connectionUrl || "default" });
62
+ } catch (error) {
63
+ console.error("Failed to connect:", error);
64
+ addEvent("system", "error", { error: String(error) });
65
+ }
66
+ };
67
+
68
+ const handleDisconnect = () => {
69
+ if (wsProviderRef.current) {
70
+ wsProviderRef.current.disconnect();
71
+ wsProviderRef.current = null;
72
+ setIsConnected(false);
73
+ setSubscriptions(new Set());
74
+ addEvent("system", "disconnected", {});
75
+ }
76
+ };
77
+
78
+ const handleSubscribe = () => {
79
+ if (!wsProviderRef.current || !isConnected) {
80
+ addEvent("system", "error", { error: "Not connected to websocket" });
81
+ return;
82
+ }
83
+
84
+ const subscriptionKey = `${channel}#${eventName}`;
85
+
86
+ if (subscriptions.has(subscriptionKey)) {
87
+ addEvent("system", "warning", { warning: `Already subscribed to ${subscriptionKey}` });
88
+ return;
89
+ }
90
+
91
+ try {
92
+ // Bind to a private channel (user-specific)
93
+ wsProviderRef.current.bind(channel, eventName, (data: any) => {
94
+ addEvent(channel, eventName, data);
95
+ });
96
+
97
+ setSubscriptions(new Set([...subscriptions, subscriptionKey]));
98
+ addEvent("system", "subscribed", { channel, event: eventName });
99
+ } catch (error) {
100
+ console.error("Failed to subscribe:", error);
101
+ addEvent("system", "error", { error: String(error) });
102
+ }
103
+ };
104
+
105
+ const handleSubscribeGlobal = () => {
106
+ if (!wsProviderRef.current || !isConnected) {
107
+ addEvent("system", "error", { error: "Not connected to websocket" });
108
+ return;
109
+ }
110
+
111
+ const globalChannel = channel;
112
+
113
+ if (subscriptions.has(`global:${globalChannel}`)) {
114
+ addEvent("system", "warning", { warning: `Already subscribed to global channel ${globalChannel}` });
115
+ return;
116
+ }
117
+
118
+ try {
119
+ // Bind to a global channel (not user-specific)
120
+ wsProviderRef.current.bindGlobal(globalChannel, (data: any) => {
121
+ addEvent(`global:${globalChannel}`, "publication", data);
122
+ });
123
+
124
+ setSubscriptions(new Set([...subscriptions, `global:${globalChannel}`]));
125
+ addEvent("system", "subscribed", { channel: `global:${globalChannel}` });
126
+ } catch (error) {
127
+ console.error("Failed to subscribe to global channel:", error);
128
+ addEvent("system", "error", { error: String(error) });
129
+ }
130
+ };
131
+
132
+ const handleUnsubscribe = () => {
133
+ if (!wsProviderRef.current || !isConnected) {
134
+ addEvent("system", "error", { error: "Not connected to websocket" });
135
+ return;
136
+ }
137
+
138
+ const subscriptionKey = `${channel}#${eventName}`;
139
+
140
+ try {
141
+ wsProviderRef.current.unbind(channel, eventName);
142
+ const newSubscriptions = new Set(subscriptions);
143
+ newSubscriptions.delete(subscriptionKey);
144
+ setSubscriptions(newSubscriptions);
145
+ addEvent("system", "unsubscribed", { channel, event: eventName });
146
+ } catch (error) {
147
+ console.error("Failed to unsubscribe:", error);
148
+ addEvent("system", "error", { error: String(error) });
149
+ }
150
+ };
151
+
152
+ const handleUnsubscribeAll = () => {
153
+ if (!wsProviderRef.current || !isConnected) {
154
+ addEvent("system", "error", { error: "Not connected to websocket" });
155
+ return;
156
+ }
157
+
158
+ try {
159
+ wsProviderRef.current.unbindAll(channel);
160
+ const newSubscriptions = new Set(
161
+ [...subscriptions].filter(sub => !sub.startsWith(channel))
162
+ );
163
+ setSubscriptions(newSubscriptions);
164
+ addEvent("system", "unsubscribed-all", { channel });
165
+ } catch (error) {
166
+ console.error("Failed to unsubscribe all:", error);
167
+ addEvent("system", "error", { error: String(error) });
168
+ }
169
+ };
170
+
171
+ const addEvent = (channel: string, event: string, data: any) => {
172
+ const newEvent: WebSocketEvent = {
173
+ id: `${Date.now()}-${Math.random()}`,
174
+ timestamp: new Date(),
175
+ channel,
176
+ event,
177
+ data
178
+ };
179
+ setEvents(prev => [newEvent, ...prev].slice(0, 100)); // Keep last 100 events
180
+ };
181
+
182
+ const clearEvents = () => {
183
+ setEvents([]);
184
+ };
185
+
186
+ return (
187
+ <SidebarProvider
188
+ style={
189
+ {
190
+ "--sidebar-width": "calc(var(--spacing) * 72)",
191
+ "--header-height": "calc(var(--spacing) * 12)",
192
+ } as React.CSSProperties
193
+ }
194
+ >
195
+ <AppSidebar variant="inset" />
196
+ <SidebarInset>
197
+ <SiteHeader />
198
+ <div className="flex flex-1 flex-col">
199
+ <div className="flex flex-1 flex-col gap-4 p-4 md:gap-6 md:p-6">
200
+ <div className="flex flex-col gap-2">
201
+ <h1 className="text-3xl font-bold">WebSocket Testing</h1>
202
+ <p className="text-muted-foreground">
203
+ Test websocket connections using @bluecopa/core utilities
204
+ </p>
205
+ </div>
206
+
207
+ <div className="grid gap-4 md:grid-cols-2">
208
+ {/* Connection Panel */}
209
+ <Card>
210
+ <CardHeader>
211
+ <CardTitle className="flex items-center gap-2">
212
+ Connection
213
+ <Badge variant={isConnected ? "default" : "secondary"}>
214
+ {isConnected ? "Connected" : "Disconnected"}
215
+ </Badge>
216
+ </CardTitle>
217
+ <CardDescription>
218
+ Connect to the Centrifugo websocket server
219
+ </CardDescription>
220
+ </CardHeader>
221
+ <CardContent className="space-y-4">
222
+ <div className="space-y-2">
223
+ <Label htmlFor="connection-url">
224
+ Connection URL (optional)
225
+ </Label>
226
+ <Input
227
+ id="connection-url"
228
+ placeholder="wss://your-centrifugo-server.com/connection/websocket"
229
+ value={connectionUrl}
230
+ onChange={(e) => setConnectionUrl(e.target.value)}
231
+ disabled={isConnected}
232
+ />
233
+ <p className="text-xs text-muted-foreground">
234
+ Leave empty to use the default configured URL
235
+ </p>
236
+ </div>
237
+ <div className="flex gap-2">
238
+ <Button
239
+ onClick={handleConnect}
240
+ disabled={isConnected}
241
+ className="flex-1"
242
+ >
243
+ Connect
244
+ </Button>
245
+ <Button
246
+ onClick={handleDisconnect}
247
+ disabled={!isConnected}
248
+ variant="destructive"
249
+ className="flex-1"
250
+ >
251
+ Disconnect
252
+ </Button>
253
+ </div>
254
+ </CardContent>
255
+ </Card>
256
+
257
+ {/* Subscription Panel */}
258
+ <Card>
259
+ <CardHeader>
260
+ <CardTitle>Subscriptions</CardTitle>
261
+ <CardDescription>
262
+ Subscribe to channels and events
263
+ </CardDescription>
264
+ </CardHeader>
265
+ <CardContent className="space-y-4">
266
+ <div className="space-y-2">
267
+ <Label htmlFor="channel">Channel</Label>
268
+ <Input
269
+ id="channel"
270
+ placeholder="test-channel"
271
+ value={channel}
272
+ onChange={(e) => setChannel(e.target.value)}
273
+ disabled={!isConnected}
274
+ />
275
+ </div>
276
+ <div className="space-y-2">
277
+ <Label htmlFor="event">Event Name</Label>
278
+ <Input
279
+ id="event"
280
+ placeholder="message"
281
+ value={eventName}
282
+ onChange={(e) => setEventName(e.target.value)}
283
+ disabled={!isConnected}
284
+ />
285
+ </div>
286
+ <div className="flex flex-col gap-2">
287
+ <div className="flex gap-2">
288
+ <Button
289
+ onClick={handleSubscribe}
290
+ disabled={!isConnected}
291
+ className="flex-1"
292
+ size="sm"
293
+ >
294
+ Subscribe (Private)
295
+ </Button>
296
+ <Button
297
+ onClick={handleSubscribeGlobal}
298
+ disabled={!isConnected}
299
+ variant="outline"
300
+ className="flex-1"
301
+ size="sm"
302
+ >
303
+ Subscribe (Global)
304
+ </Button>
305
+ </div>
306
+ <div className="flex gap-2">
307
+ <Button
308
+ onClick={handleUnsubscribe}
309
+ disabled={!isConnected}
310
+ variant="secondary"
311
+ className="flex-1"
312
+ size="sm"
313
+ >
314
+ Unsubscribe
315
+ </Button>
316
+ <Button
317
+ onClick={handleUnsubscribeAll}
318
+ disabled={!isConnected}
319
+ variant="destructive"
320
+ className="flex-1"
321
+ size="sm"
322
+ >
323
+ Unsubscribe All
324
+ </Button>
325
+ </div>
326
+ </div>
327
+ </CardContent>
328
+ </Card>
329
+ </div>
330
+
331
+ {/* Active Subscriptions */}
332
+ <Card>
333
+ <CardHeader>
334
+ <CardTitle>Active Subscriptions ({subscriptions.size})</CardTitle>
335
+ </CardHeader>
336
+ <CardContent>
337
+ {subscriptions.size === 0 ? (
338
+ <p className="text-sm text-muted-foreground">No active subscriptions</p>
339
+ ) : (
340
+ <div className="flex flex-wrap gap-2">
341
+ {[...subscriptions].map((sub) => (
342
+ <Badge key={sub} variant="outline">
343
+ {sub}
344
+ </Badge>
345
+ ))}
346
+ </div>
347
+ )}
348
+ </CardContent>
349
+ </Card>
350
+
351
+ {/* Events Log */}
352
+ <Card>
353
+ <CardHeader>
354
+ <div className="flex items-center justify-between">
355
+ <div>
356
+ <CardTitle>Events Log ({events.length})</CardTitle>
357
+ <CardDescription>Real-time websocket events</CardDescription>
358
+ </div>
359
+ <Button
360
+ onClick={clearEvents}
361
+ variant="outline"
362
+ size="sm"
363
+ >
364
+ Clear
365
+ </Button>
366
+ </div>
367
+ </CardHeader>
368
+ <CardContent>
369
+ <div className="space-y-2 max-h-96 overflow-y-auto">
370
+ {events.length === 0 ? (
371
+ <p className="text-sm text-muted-foreground">No events yet</p>
372
+ ) : (
373
+ events.map((event) => (
374
+ <div
375
+ key={event.id}
376
+ className="rounded-lg border p-3 space-y-1"
377
+ >
378
+ <div className="flex items-center justify-between">
379
+ <div className="flex items-center gap-2">
380
+ <Badge variant={event.channel === "system" ? "secondary" : "default"}>
381
+ {event.channel}
382
+ </Badge>
383
+ <span className="text-sm font-mono">{event.event}</span>
384
+ </div>
385
+ <span className="text-xs text-muted-foreground">
386
+ {event.timestamp.toLocaleTimeString()}
387
+ </span>
388
+ </div>
389
+ <Separator />
390
+ <pre className="text-xs bg-muted p-2 rounded overflow-x-auto">
391
+ {JSON.stringify(event.data, null, 2)}
392
+ </pre>
393
+ </div>
394
+ ))
395
+ )}
396
+ </div>
397
+ </CardContent>
398
+ </Card>
399
+
400
+ {/* Usage Instructions */}
401
+ <Card>
402
+ <CardHeader>
403
+ <CardTitle>Usage Instructions</CardTitle>
404
+ </CardHeader>
405
+ <CardContent className="space-y-4">
406
+ <div className="space-y-2">
407
+ <h4 className="font-semibold">Before Using:</h4>
408
+ <pre className="text-xs bg-muted p-3 rounded overflow-x-auto">
409
+ {`import { copaSetConfig } from "@bluecopa/core";
410
+
411
+ // Set configuration before connecting
412
+ copaSetConfig({
413
+ apiBaseUrl: "https://api.example.com",
414
+ accessToken: "your-access-token",
415
+ workspaceId: "your-workspace-id",
416
+ userId: "your-user-id"
417
+ });`}
418
+ </pre>
419
+ </div>
420
+ <div className="space-y-2">
421
+ <h4 className="font-semibold">Channel Types:</h4>
422
+ <ul className="list-disc list-inside text-sm space-y-1 text-muted-foreground">
423
+ <li>
424
+ <strong>Private:</strong> Format is{" "}
425
+ <code className="bg-muted px-1 rounded">private-{"{channel}"}-{"{userId}"}#{"{event}"}</code>
426
+ </li>
427
+ <li>
428
+ <strong>Global:</strong> Format is just the channel name, accessible by all users
429
+ </li>
430
+ </ul>
431
+ </div>
432
+ <div className="space-y-2">
433
+ <h4 className="font-semibold">Example Usage:</h4>
434
+ <ol className="list-decimal list-inside text-sm space-y-1 text-muted-foreground">
435
+ <li>Click "Connect" to establish a websocket connection</li>
436
+ <li>Enter a channel name (e.g., "notifications")</li>
437
+ <li>Enter an event name (e.g., "update")</li>
438
+ <li>Click "Subscribe (Private)" or "Subscribe (Global)"</li>
439
+ <li>Events will appear in the log below when received</li>
440
+ </ol>
441
+ </div>
442
+ </CardContent>
443
+ </Card>
444
+ </div>
445
+ </div>
446
+ </SidebarInset>
447
+ </SidebarProvider>
448
+ );
449
+ }
@@ -3,12 +3,18 @@ import { lazy } from "react";
3
3
  import "./app.css";
4
4
 
5
5
  const Home = lazy(() => import('~/routes/home'))
6
+ const Comments = lazy(() => import('~/routes/comments'))
7
+ const Websocket = lazy(() => import('~/routes/websocket'))
8
+ const Payments = lazy(() => import('~/routes/payments'))
6
9
 
7
10
 
8
11
  export default function RouteConfig() {
9
12
  return (
10
13
  <Routes>
11
14
  <Route path="/" element={<Home />} />
15
+ <Route path="/comments" element={<Comments />} />
16
+ <Route path="/websocket" element={<Websocket />} />
17
+ <Route path="/payments" element={<Payments />} />
12
18
  <Route path="*" element={<Navigate to="/" replace />} />
13
19
  </Routes>
14
20
  );
@@ -1,12 +1,13 @@
1
- System.register(['./__federation_fn_import-CzfA7kmP.js', './client-Hh38T4k9.js'], (function (exports, module) {
1
+ System.register(['./__federation_fn_import-CzfA7kmP.js', './index-D5og7-RT-BA7DwZw1.js', './client-LFBsfOjG.js'], (function (exports, module) {
2
2
  'use strict';
3
- var importShared, clientExports, jsxRuntimeExports, MemoryRouter, BrowserRouter, App;
3
+ var importShared, jsxRuntimeExports, clientExports, MemoryRouter, BrowserRouter, App;
4
4
  return {
5
5
  setters: [module => {
6
6
  importShared = module.importShared;
7
7
  }, module => {
8
- clientExports = module.c;
9
8
  jsxRuntimeExports = module.j;
9
+ }, module => {
10
+ clientExports = module.c;
10
11
  MemoryRouter = module.M;
11
12
  BrowserRouter = module.B;
12
13
  App = module.A;
@@ -55,7 +56,17 @@ System.register(['./__federation_fn_import-CzfA7kmP.js', './client-Hh38T4k9.js']
55
56
  root = null;
56
57
  }
57
58
  root = clientExports.createRoot(props.domElement);
58
- root.render(/* @__PURE__ */ jsxRuntimeExports.jsx(MicrofrontendRoot, { isMicroFrontend: true }));
59
+ root.render(
60
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
61
+ MicrofrontendRoot,
62
+ {
63
+ isMicroFrontend: true,
64
+ props: {
65
+ basename: props.basename || "/app/external/microfrontend"
66
+ }
67
+ }
68
+ )
69
+ );
59
70
  console.log("Microfrontend mounted successfully");
60
71
  return;
61
72
  } catch (error) {