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.
- package/bin/create-bluecopa-react-app.js +1 -1
- package/package.json +1 -1
- package/templates/latest/.dockerignore +5 -1
- package/templates/latest/Agent.md +577 -0
- package/templates/latest/Dockerfile +2 -2
- package/templates/latest/app/app.tsx +3 -1
- package/templates/latest/app/components/app-sidebar.tsx +14 -16
- package/templates/latest/app/components/nav-main.tsx +6 -22
- package/templates/latest/app/data/mock-payments.json +122 -0
- package/templates/latest/app/data/mock-transactions.json +128 -0
- package/templates/latest/app/routes/comments.tsx +552 -0
- package/templates/latest/app/routes/home.tsx +1 -1
- package/templates/latest/app/routes/payments.tsx +342 -0
- package/templates/latest/app/routes/websocket.tsx +449 -0
- package/templates/latest/app/routes.tsx +6 -0
- package/templates/latest/dist/assets/{__federation_expose_App-C8_sl1dD.js → __federation_expose_App-B2IoFaIA.js} +15 -4
- package/templates/latest/dist/assets/client-LFBsfOjG.js +2775 -0
- package/templates/latest/dist/assets/{home-DhyEFlEc.js → home-BBY02MnI.js} +87 -59
- package/templates/latest/dist/assets/{index-DkyIpbj3.js → index-CNNS7Foy.js} +4 -3
- package/templates/latest/dist/assets/{client-Hh38T4k9.js → index-D5og7-RT-BA7DwZw1.js} +46 -2789
- package/templates/latest/dist/assets/remoteEntry.css +4 -4
- package/templates/latest/dist/assets/remoteEntry.js +1 -1
- package/templates/latest/dist/index.html +3 -2
- package/templates/latest/package-lock.json +11 -11
- package/templates/latest/package.json +1 -1
- package/templates/latest/public/favicon.ico +0 -0
- package/templates/latest/public/avatars/shadcn.svg +0 -6
- /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 "~/
|
|
7
|
+
import data from "~/data/data.json";
|
|
8
8
|
|
|
9
9
|
export default function Page() {
|
|
10
10
|
return (
|