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,552 @@
1
+ import React, { useState } 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 { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
6
+ import { Button } from "~/components/ui/button";
7
+ import { Input } from "~/components/ui/input";
8
+ import { Label } from "~/components/ui/label";
9
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select";
10
+ import { Badge } from "~/components/ui/badge";
11
+ import { Separator } from "~/components/ui/separator";
12
+ import { Avatar, AvatarFallback } from "~/components/ui/avatar";
13
+ import { Skeleton } from "~/components/ui/skeleton";
14
+ import { toast } from "sonner";
15
+ import {
16
+ useCreateThread,
17
+ useGetCommentsByThreadId,
18
+ usePostComment,
19
+ useUpdateComment,
20
+ useDeleteComment,
21
+ useSubscribeUser,
22
+ useUnsubscribeUser,
23
+ useCheckSubscriptionStatus,
24
+ copaApi
25
+ } from '@bluecopa/react';
26
+ import { MessageSquare, Plus, Send, Edit, Trash2, Bell, BellOff, Users } from 'lucide-react';
27
+
28
+ // Mock user data - in real app this would come from auth context
29
+ const MOCK_USER = {
30
+ id: 'user-123',
31
+ name: 'John Doe',
32
+ email: 'john@example.com'
33
+ };
34
+
35
+ // Component types for better organization
36
+ interface ThreadFormProps {
37
+ onThreadCreated: (threadId: string) => void;
38
+ }
39
+
40
+ interface CommentFormProps {
41
+ threadId: string;
42
+ onCommentPosted: () => void;
43
+ }
44
+
45
+ interface CommentItemProps {
46
+ comment: any;
47
+ onCommentUpdated: () => void;
48
+ onCommentDeleted: () => void;
49
+ }
50
+
51
+ interface SubscriptionControlsProps {
52
+ userId: string;
53
+ threadId: string;
54
+ }
55
+
56
+ // Thread Creation Form Component
57
+ function ThreadForm({ onThreadCreated }: ThreadFormProps) {
58
+ const [componentId, setComponentId] = useState('');
59
+ const [componentType, setComponentType] = useState<string>('');
60
+
61
+ const createThreadMutation = useCreateThread({
62
+ onSuccess: (data) => {
63
+ toast.success('Thread created successfully!');
64
+ onThreadCreated(data.data.id || '');
65
+ setComponentId('');
66
+ setComponentType('');
67
+ },
68
+ onError: (error) => {
69
+ toast.error(`Failed to create thread: ${error.message}`);
70
+ },
71
+ });
72
+
73
+ const handleSubmit = (e: React.FormEvent) => {
74
+ e.preventDefault();
75
+ if (!componentId || !componentType) {
76
+ toast.error('Please fill in all fields');
77
+ return;
78
+ }
79
+
80
+ createThreadMutation.mutate({
81
+ data: {
82
+ forComponentId: componentId,
83
+ forComponentType: componentType as any,
84
+ },
85
+ });
86
+ };
87
+
88
+ return (
89
+ <Card>
90
+ <CardHeader>
91
+ <CardTitle className="flex items-center gap-2">
92
+ <Plus className="h-5 w-5" />
93
+ Create New Thread
94
+ </CardTitle>
95
+ <CardDescription>
96
+ Start a new discussion thread for a component
97
+ </CardDescription>
98
+ </CardHeader>
99
+ <CardContent>
100
+ <form onSubmit={handleSubmit} className="space-y-4">
101
+ <div>
102
+ <Label htmlFor="componentId">Component ID</Label>
103
+ <Input
104
+ id="componentId"
105
+ value={componentId}
106
+ onChange={(e) => setComponentId(e.target.value)}
107
+ placeholder="Enter component ID (e.g., workbook-123)"
108
+ />
109
+ </div>
110
+ <div>
111
+ <Label htmlFor="componentType">Component Type</Label>
112
+ <Select value={componentType} onValueChange={setComponentType}>
113
+ <SelectTrigger>
114
+ <SelectValue placeholder="Select component type" />
115
+ </SelectTrigger>
116
+ <SelectContent>
117
+ <SelectItem value="WORKBOOK">Workbook</SelectItem>
118
+ <SelectItem value="DASHBOARD">Dashboard</SelectItem>
119
+ <SelectItem value="WORKSHEET">Worksheet</SelectItem>
120
+ <SelectItem value="DATASET">Dataset</SelectItem>
121
+ <SelectItem value="PROCESS">Process</SelectItem>
122
+ <SelectItem value="FORM">Form</SelectItem>
123
+ </SelectContent>
124
+ </Select>
125
+ </div>
126
+ <Button
127
+ type="submit"
128
+ disabled={createThreadMutation.isPending}
129
+ className="w-full"
130
+ >
131
+ {createThreadMutation.isPending ? 'Creating...' : 'Create Thread'}
132
+ </Button>
133
+ </form>
134
+ </CardContent>
135
+ </Card>
136
+ );
137
+ }
138
+
139
+ // Comment Form Component
140
+ function CommentForm({ threadId, onCommentPosted }: CommentFormProps) {
141
+ const [comment, setComment] = useState('');
142
+
143
+ const postCommentMutation = usePostComment({
144
+ onSuccess: () => {
145
+ toast.success('Comment posted successfully!');
146
+ setComment('');
147
+ onCommentPosted();
148
+ },
149
+ onError: (error) => {
150
+ toast.error(`Failed to post comment: ${error.message}`);
151
+ },
152
+ });
153
+
154
+ const handleSubmit = (e: React.FormEvent) => {
155
+ e.preventDefault();
156
+ if (!comment.trim()) {
157
+ toast.error('Please enter a comment');
158
+ return;
159
+ }
160
+
161
+ postCommentMutation.mutate({
162
+ data: {
163
+ chatThreadId: threadId,
164
+ comment: comment.trim(),
165
+ parentId: threadId
166
+ },
167
+ });
168
+ };
169
+
170
+ return (
171
+ <Card>
172
+ <CardHeader>
173
+ <CardTitle className="flex items-center gap-2">
174
+ <Send className="h-5 w-5" />
175
+ Add Comment
176
+ </CardTitle>
177
+ </CardHeader>
178
+ <CardContent>
179
+ <form onSubmit={handleSubmit} className="space-y-4">
180
+ <div>
181
+ <Label htmlFor="comment">Your Comment</Label>
182
+ <Input
183
+ id="comment"
184
+ value={comment}
185
+ onChange={(e) => setComment(e.target.value)}
186
+ placeholder="Enter your comment..."
187
+ className="min-h-[80px]"
188
+ />
189
+ </div>
190
+ <Button
191
+ type="submit"
192
+ disabled={postCommentMutation.isPending}
193
+ className="w-full"
194
+ >
195
+ {postCommentMutation.isPending ? 'Posting...' : 'Post Comment'}
196
+ </Button>
197
+ </form>
198
+ </CardContent>
199
+ </Card>
200
+ );
201
+ }
202
+
203
+ // Individual Comment Component
204
+ function CommentItem({ comment, onCommentUpdated, onCommentDeleted }: CommentItemProps) {
205
+ const [isEditing, setIsEditing] = useState(false);
206
+ const [editText, setEditText] = useState(comment.comment || '');
207
+
208
+ const updateCommentMutation = useUpdateComment({
209
+ onSuccess: () => {
210
+ toast.success('Comment updated successfully!');
211
+ setIsEditing(false);
212
+ onCommentUpdated();
213
+ },
214
+ onError: (error) => {
215
+ toast.error(`Failed to update comment: ${error.message}`);
216
+ },
217
+ });
218
+
219
+ const deleteCommentMutation = useDeleteComment({
220
+ onSuccess: () => {
221
+ toast.success('Comment deleted successfully!');
222
+ onCommentDeleted();
223
+ },
224
+ onError: (error) => {
225
+ toast.error(`Failed to delete comment: ${error.message}`);
226
+ },
227
+ });
228
+
229
+ const handleUpdate = () => {
230
+ if (!editText.trim()) {
231
+ toast.error('Comment cannot be empty');
232
+ return;
233
+ }
234
+
235
+ updateCommentMutation.mutate({
236
+ data: {
237
+ ...comment,
238
+ comment: editText.trim(),
239
+ },
240
+ commentId: comment.id,
241
+ });
242
+ };
243
+
244
+ const handleDelete = () => {
245
+ if (window.confirm('Are you sure you want to delete this comment?')) {
246
+ deleteCommentMutation.mutate(comment.id);
247
+ }
248
+ };
249
+
250
+ return (
251
+ <div className="border rounded-lg p-4 space-y-3">
252
+ <div className="flex items-start justify-between">
253
+ <div className="flex items-center gap-3">
254
+ <Avatar className="h-8 w-8">
255
+ <AvatarFallback>
256
+ {comment.createdBy?.charAt(0)?.toUpperCase() || 'U'}
257
+ </AvatarFallback>
258
+ </Avatar>
259
+ <div>
260
+ <p className="font-medium text-sm">{comment.createdBy || 'Unknown User'}</p>
261
+ <p className="text-xs text-muted-foreground">
262
+ {comment.createdDate ? new Date(comment.createdDate).toLocaleString() : 'Just now'}
263
+ </p>
264
+ </div>
265
+ </div>
266
+ <div className="flex items-center gap-2">
267
+ <Button
268
+ variant="ghost"
269
+ size="sm"
270
+ onClick={() => setIsEditing(!isEditing)}
271
+ disabled={updateCommentMutation.isPending}
272
+ >
273
+ <Edit className="h-4 w-4" />
274
+ </Button>
275
+ <Button
276
+ variant="ghost"
277
+ size="sm"
278
+ onClick={handleDelete}
279
+ disabled={deleteCommentMutation.isPending}
280
+ >
281
+ <Trash2 className="h-4 w-4" />
282
+ </Button>
283
+ </div>
284
+ </div>
285
+
286
+ {isEditing ? (
287
+ <div className="space-y-2">
288
+ <Input
289
+ value={editText}
290
+ onChange={(e) => setEditText(e.target.value)}
291
+ className="min-h-[60px]"
292
+ />
293
+ <div className="flex gap-2">
294
+ <Button
295
+ size="sm"
296
+ onClick={handleUpdate}
297
+ disabled={updateCommentMutation.isPending}
298
+ >
299
+ {updateCommentMutation.isPending ? 'Saving...' : 'Save'}
300
+ </Button>
301
+ <Button
302
+ size="sm"
303
+ variant="outline"
304
+ onClick={() => {
305
+ setIsEditing(false);
306
+ setEditText(comment.comment || '');
307
+ }}
308
+ >
309
+ Cancel
310
+ </Button>
311
+ </div>
312
+ </div>
313
+ ) : (
314
+ <p className="text-sm">{comment.comment}</p>
315
+ )}
316
+ </div>
317
+ );
318
+ }
319
+
320
+ // Subscription Controls Component
321
+ function SubscriptionControls({ userId, threadId }: SubscriptionControlsProps) {
322
+ const { data: subscriptionStatus, isLoading } = useCheckSubscriptionStatus(userId, threadId);
323
+
324
+ const subscribeUserMutation = useSubscribeUser({
325
+ onSuccess: () => {
326
+ toast.success('Successfully subscribed to thread!');
327
+ },
328
+ onError: (error) => {
329
+ toast.error(`Failed to subscribe: ${error.message}`);
330
+ },
331
+ });
332
+
333
+ const unsubscribeUserMutation = useUnsubscribeUser({
334
+ onSuccess: () => {
335
+ toast.success('Successfully unsubscribed from thread!');
336
+ },
337
+ onError: (error) => {
338
+ toast.error(`Failed to unsubscribe: ${error.message}`);
339
+ },
340
+ });
341
+
342
+ const handleSubscribe = () => {
343
+ subscribeUserMutation.mutate({
344
+ params: { userId, threadId },
345
+ });
346
+ };
347
+
348
+ const handleUnsubscribe = () => {
349
+ unsubscribeUserMutation.mutate({
350
+ params: { userId, threadId },
351
+ });
352
+ };
353
+
354
+ if (isLoading) {
355
+ return <Skeleton className="h-10 w-32" />;
356
+ }
357
+
358
+ const isSubscribed = subscriptionStatus?.data?.isSubscribed;
359
+
360
+ return (
361
+ <div className="flex items-center gap-2">
362
+ <Button
363
+ variant={isSubscribed ? "outline" : "default"}
364
+ size="sm"
365
+ onClick={isSubscribed ? handleUnsubscribe : handleSubscribe}
366
+ disabled={subscribeUserMutation.isPending || unsubscribeUserMutation.isPending}
367
+ >
368
+ {isSubscribed ? (
369
+ <>
370
+ <BellOff className="h-4 w-4 mr-2" />
371
+ Unsubscribe
372
+ </>
373
+ ) : (
374
+ <>
375
+ <Bell className="h-4 w-4 mr-2" />
376
+ Subscribe
377
+ </>
378
+ )}
379
+ </Button>
380
+ <Badge variant={isSubscribed ? "default" : "secondary"}>
381
+ {isSubscribed ? "Subscribed" : "Not Subscribed"}
382
+ </Badge>
383
+ </div>
384
+ );
385
+ }
386
+
387
+ // Main Comments Page Component
388
+ export default function CommentsPage() {
389
+ const [selectedThreadId, setSelectedThreadId] = useState<string>('');
390
+
391
+ const {
392
+ data: commentsData,
393
+ isLoading: commentsLoading,
394
+ refetch: refetchComments
395
+ } = useGetCommentsByThreadId(selectedThreadId, {
396
+ enabled: !!selectedThreadId,
397
+ });
398
+
399
+ const handleThreadCreated = (threadId: string) => {
400
+ setSelectedThreadId(threadId);
401
+ };
402
+
403
+ const handleCommentAction = () => {
404
+ refetchComments();
405
+ };
406
+
407
+ return (
408
+ <SidebarProvider
409
+ style={
410
+ {
411
+ "--sidebar-width": "calc(var(--spacing) * 72)",
412
+ "--header-height": "calc(var(--spacing) * 12)",
413
+ } as React.CSSProperties
414
+ }
415
+ >
416
+ <AppSidebar variant="inset" />
417
+ <SidebarInset>
418
+ <SiteHeader />
419
+ <div className="flex flex-1 flex-col">
420
+ <div className="@container/main flex flex-1 flex-col gap-6 p-6">
421
+ {/* Page Header */}
422
+ <div className="flex items-center justify-between">
423
+ <div>
424
+ <h1 className="text-3xl font-bold tracking-tight flex items-center gap-2">
425
+ <MessageSquare className="h-8 w-8" />
426
+ Comments API Demo
427
+ </h1>
428
+ <p className="text-muted-foreground">
429
+ Comprehensive example of the chat/comments API functionality
430
+ </p>
431
+ </div>
432
+ {selectedThreadId && (
433
+ <Badge variant="outline" className="text-sm">
434
+ Thread: {selectedThreadId.slice(0, 8)}...
435
+ </Badge>
436
+ )}
437
+ </div>
438
+
439
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
440
+ {/* Left Column - Thread Creation */}
441
+ <div className="space-y-6">
442
+ <ThreadForm onThreadCreated={handleThreadCreated} />
443
+
444
+ {selectedThreadId && (
445
+ <>
446
+ <CommentForm
447
+ threadId={selectedThreadId}
448
+ onCommentPosted={handleCommentAction}
449
+ />
450
+
451
+ <Card>
452
+ <CardHeader>
453
+ <CardTitle className="flex items-center gap-2">
454
+ <Users className="h-5 w-5" />
455
+ Subscription Controls
456
+ </CardTitle>
457
+ <CardDescription>
458
+ Manage your subscription to this thread
459
+ </CardDescription>
460
+ </CardHeader>
461
+ <CardContent>
462
+ <SubscriptionControls
463
+ userId={MOCK_USER.id}
464
+ threadId={selectedThreadId}
465
+ />
466
+ </CardContent>
467
+ </Card>
468
+ </>
469
+ )}
470
+ </div>
471
+
472
+ {/* Right Column - Comments Display */}
473
+ <div className="space-y-6">
474
+ <Card>
475
+ <CardHeader>
476
+ <CardTitle>Comments</CardTitle>
477
+ <CardDescription>
478
+ {selectedThreadId
479
+ ? `Comments for thread ${selectedThreadId.slice(0, 8)}...`
480
+ : 'Create a thread to see comments'
481
+ }
482
+ </CardDescription>
483
+ </CardHeader>
484
+ <CardContent>
485
+ {!selectedThreadId ? (
486
+ <div className="text-center py-8 text-muted-foreground">
487
+ <MessageSquare className="h-12 w-12 mx-auto mb-4 opacity-50" />
488
+ <p>Create a thread to start the conversation</p>
489
+ </div>
490
+ ) : commentsLoading ? (
491
+ <div className="space-y-4">
492
+ {[1, 2, 3].map((i) => (
493
+ <div key={i} className="space-y-2">
494
+ <Skeleton className="h-4 w-32" />
495
+ <Skeleton className="h-16 w-full" />
496
+ </div>
497
+ ))}
498
+ </div>
499
+ ) : commentsData?.data?.length === 0 ? (
500
+ <div className="text-center py-8 text-muted-foreground">
501
+ <MessageSquare className="h-12 w-12 mx-auto mb-4 opacity-50" />
502
+ <p>No comments yet. Be the first to comment!</p>
503
+ </div>
504
+ ) : (
505
+ <div className="space-y-4 max-h-96 overflow-y-auto">
506
+ {commentsData?.data?.map((comment: any) => (
507
+ <CommentItem
508
+ key={comment.id}
509
+ comment={comment}
510
+ onCommentUpdated={handleCommentAction}
511
+ onCommentDeleted={handleCommentAction}
512
+ />
513
+ ))}
514
+ </div>
515
+ )}
516
+ </CardContent>
517
+ </Card>
518
+
519
+ {/* API Status Card */}
520
+ <Card>
521
+ <CardHeader>
522
+ <CardTitle>API Status</CardTitle>
523
+ </CardHeader>
524
+ <CardContent className="space-y-2">
525
+ <div className="flex justify-between items-center">
526
+ <span className="text-sm">Thread Selected:</span>
527
+ <Badge variant={selectedThreadId ? "default" : "secondary"}>
528
+ {selectedThreadId ? "Yes" : "No"}
529
+ </Badge>
530
+ </div>
531
+ <div className="flex justify-between items-center">
532
+ <span className="text-sm">Comments Loading:</span>
533
+ <Badge variant={commentsLoading ? "default" : "secondary"}>
534
+ {commentsLoading ? "Yes" : "No"}
535
+ </Badge>
536
+ </div>
537
+ <div className="flex justify-between items-center">
538
+ <span className="text-sm">Comments Count:</span>
539
+ <Badge variant="outline">
540
+ {commentsData?.data?.length || 0}
541
+ </Badge>
542
+ </div>
543
+ </CardContent>
544
+ </Card>
545
+ </div>
546
+ </div>
547
+ </div>
548
+ </div>
549
+ </SidebarInset>
550
+ </SidebarProvider>
551
+ );
552
+ }
@@ -4,7 +4,7 @@ import { DataTable } from "~/components/data-table";
4
4
  import { SectionCards } from "~/components/section-cards";
5
5
  import { SiteHeader } from "~/components/site-header";
6
6
  import { SidebarInset, SidebarProvider } from "~/components/ui/sidebar";
7
- import data from "~/dashboard/data.json";
7
+ import data from "~/data/data.json";
8
8
 
9
9
  export default function Page() {
10
10
  return (