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