@moontra/moonui-pro 2.12.0 → 2.14.0

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.
@@ -0,0 +1,769 @@
1
+ "use client"
2
+
3
+ import React, { useState, useEffect } from 'react'
4
+ import { motion, AnimatePresence } from 'framer-motion'
5
+ import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../ui/dialog'
6
+ import { Button } from '../ui/button'
7
+ import { Input } from '../ui/input'
8
+ import { Textarea } from '../ui/textarea'
9
+ import { Label } from '../ui/label'
10
+ import { MoonUIBadgePro as Badge } from '../ui/badge'
11
+ import { MoonUIAvatarPro, MoonUIAvatarFallbackPro, MoonUIAvatarImagePro, AvatarGroup as MoonUIAvatarGroupPro } from '../ui/avatar'
12
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
13
+ import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'
14
+ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from '../ui/command'
15
+ import { Calendar as CalendarComponent } from '../ui/calendar'
16
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs'
17
+ import { ScrollArea } from '../ui/scroll-area'
18
+ import { Progress } from '../ui/progress'
19
+ import { Separator } from '../ui/separator'
20
+ import { Checkbox } from '../ui/checkbox'
21
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '../ui/dropdown-menu'
22
+ import {
23
+ X,
24
+ Calendar,
25
+ User,
26
+ Tag,
27
+ Paperclip,
28
+ MessageCircle,
29
+ Clock,
30
+ Flag,
31
+ CheckSquare,
32
+ Plus,
33
+ MoreHorizontal,
34
+ Edit2,
35
+ Trash2,
36
+ Download,
37
+ Eye,
38
+ Link2,
39
+ Archive,
40
+ Copy,
41
+ Activity,
42
+ AlertCircle,
43
+ Check,
44
+ ChevronDown,
45
+ Upload,
46
+ FileText,
47
+ Image,
48
+ FileIcon,
49
+ Send,
50
+ Star,
51
+ StarOff
52
+ } from 'lucide-react'
53
+ import { format } from 'date-fns'
54
+ import { cn } from '../../lib/utils'
55
+ import type { KanbanCard, KanbanAssignee, KanbanLabel, KanbanActivity, KanbanChecklist } from './types'
56
+
57
+ interface CardDetailModalProps {
58
+ card: KanbanCard
59
+ isOpen: boolean
60
+ onClose: () => void
61
+ onUpdate: (card: KanbanCard) => void
62
+ onDelete: (card: KanbanCard) => void
63
+ availableAssignees?: KanbanAssignee[]
64
+ availableLabels?: KanbanLabel[]
65
+ currentColumn?: string
66
+ availableColumns?: { id: string; title: string }[]
67
+ }
68
+
69
+ const PRIORITY_OPTIONS = [
70
+ { value: 'low', label: 'Low', icon: Flag, color: 'text-green-600' },
71
+ { value: 'medium', label: 'Medium', icon: Flag, color: 'text-yellow-600' },
72
+ { value: 'high', label: 'High', icon: Flag, color: 'text-orange-600' },
73
+ { value: 'urgent', label: 'Urgent', icon: AlertCircle, color: 'text-red-600' }
74
+ ]
75
+
76
+ const formatFileSize = (bytes: number) => {
77
+ if (bytes === 0) return '0 Bytes'
78
+ const k = 1024
79
+ const sizes = ['Bytes', 'KB', 'MB', 'GB']
80
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
81
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
82
+ }
83
+
84
+ const getInitials = (name: string) => {
85
+ return name.split(' ').map(n => n[0]).join('').toUpperCase()
86
+ }
87
+
88
+ export function CardDetailModal({
89
+ card: initialCard,
90
+ isOpen,
91
+ onClose,
92
+ onUpdate,
93
+ onDelete,
94
+ availableAssignees = [],
95
+ availableLabels = [],
96
+ currentColumn,
97
+ availableColumns = []
98
+ }: CardDetailModalProps) {
99
+ const [card, setCard] = useState(initialCard)
100
+ const [isEditingTitle, setIsEditingTitle] = useState(false)
101
+ const [isEditingDescription, setIsEditingDescription] = useState(false)
102
+ const [newComment, setNewComment] = useState('')
103
+ const [newChecklistItem, setNewChecklistItem] = useState('')
104
+ const [selectedTab, setSelectedTab] = useState('overview')
105
+
106
+ useEffect(() => {
107
+ setCard(initialCard)
108
+ }, [initialCard])
109
+
110
+ const updateCard = (updates: Partial<KanbanCard>) => {
111
+ const updatedCard = { ...card, ...updates }
112
+ setCard(updatedCard)
113
+ onUpdate(updatedCard)
114
+ }
115
+
116
+ const addComment = () => {
117
+ if (!newComment.trim()) return
118
+
119
+ const newActivity: KanbanActivity = {
120
+ id: Date.now().toString(),
121
+ user: {
122
+ id: 'current-user',
123
+ name: 'Current User',
124
+ email: 'user@example.com'
125
+ },
126
+ action: 'commented',
127
+ timestamp: new Date(),
128
+ details: newComment
129
+ }
130
+
131
+ updateCard({
132
+ activities: [newActivity, ...(card.activities || [])],
133
+ comments: (card.comments || 0) + 1
134
+ })
135
+ setNewComment('')
136
+ }
137
+
138
+ const addChecklistItem = () => {
139
+ if (!newChecklistItem.trim()) return
140
+
141
+ const checklist = card.checklist || {
142
+ id: Date.now().toString(),
143
+ title: 'Checklist',
144
+ items: []
145
+ }
146
+
147
+ checklist.items.push({
148
+ id: Date.now().toString(),
149
+ text: newChecklistItem,
150
+ completed: false
151
+ })
152
+
153
+ updateCard({ checklist })
154
+ setNewChecklistItem('')
155
+ }
156
+
157
+ const toggleChecklistItem = (itemId: string) => {
158
+ if (!card.checklist) return
159
+
160
+ const updatedChecklist = {
161
+ ...card.checklist,
162
+ items: card.checklist.items.map(item =>
163
+ item.id === itemId ? { ...item, completed: !item.completed } : item
164
+ )
165
+ }
166
+
167
+ updateCard({ checklist: updatedChecklist })
168
+ }
169
+
170
+ const deleteChecklistItem = (itemId: string) => {
171
+ if (!card.checklist) return
172
+
173
+ const updatedChecklist = {
174
+ ...card.checklist,
175
+ items: card.checklist.items.filter(item => item.id !== itemId)
176
+ }
177
+
178
+ updateCard({ checklist: updatedChecklist })
179
+ }
180
+
181
+ const completedChecklistItems = card.checklist?.items.filter(item => item.completed).length || 0
182
+ const totalChecklistItems = card.checklist?.items.length || 0
183
+ const checklistProgress = totalChecklistItems > 0 ? (completedChecklistItems / totalChecklistItems) * 100 : 0
184
+
185
+ const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
186
+ const files = e.target.files
187
+ if (!files) return
188
+
189
+ const newAttachments = Array.from(files).map(file => ({
190
+ id: Date.now().toString() + Math.random(),
191
+ name: file.name,
192
+ type: file.type,
193
+ url: URL.createObjectURL(file),
194
+ size: file.size
195
+ }))
196
+
197
+ updateCard({
198
+ attachments: [...(card.attachments || []), ...newAttachments]
199
+ })
200
+ }
201
+
202
+ const removeAttachment = (attachmentId: string) => {
203
+ updateCard({
204
+ attachments: card.attachments?.filter(a => a.id !== attachmentId)
205
+ })
206
+ }
207
+
208
+ return (
209
+ <Dialog open={isOpen} onOpenChange={onClose}>
210
+ <DialogContent className="max-w-4xl max-h-[90vh] overflow-hidden p-0">
211
+ {/* Header */}
212
+ <div className="p-6 pb-0">
213
+ <div className="flex items-start justify-between">
214
+ <div className="flex-1">
215
+ {isEditingTitle ? (
216
+ <Input
217
+ value={card.title}
218
+ onChange={(e) => updateCard({ title: e.target.value })}
219
+ onBlur={() => setIsEditingTitle(false)}
220
+ onKeyDown={(e) => {
221
+ if (e.key === 'Enter') setIsEditingTitle(false)
222
+ if (e.key === 'Escape') {
223
+ setCard(initialCard)
224
+ setIsEditingTitle(false)
225
+ }
226
+ }}
227
+ className="text-xl font-semibold"
228
+ autoFocus
229
+ />
230
+ ) : (
231
+ <h2
232
+ className="text-xl font-semibold cursor-pointer hover:bg-muted rounded px-2 -mx-2 py-1"
233
+ onClick={() => setIsEditingTitle(true)}
234
+ >
235
+ {card.title}
236
+ </h2>
237
+ )}
238
+ {currentColumn && (
239
+ <p className="text-sm text-muted-foreground mt-1">
240
+ in {currentColumn}
241
+ </p>
242
+ )}
243
+ </div>
244
+ <Button
245
+ variant="ghost"
246
+ size="icon"
247
+ onClick={onClose}
248
+ className="h-8 w-8"
249
+ >
250
+ <X className="h-4 w-4" />
251
+ </Button>
252
+ </div>
253
+ </div>
254
+
255
+ {/* Tabs */}
256
+ <Tabs value={selectedTab} onValueChange={setSelectedTab} className="flex-1">
257
+ <div className="px-6 pt-4">
258
+ <TabsList>
259
+ <TabsTrigger value="overview">Overview</TabsTrigger>
260
+ <TabsTrigger value="activity">
261
+ Activity
262
+ {card.activities && card.activities.length > 0 && (
263
+ <Badge variant="secondary" className="ml-2">
264
+ {card.activities.length}
265
+ </Badge>
266
+ )}
267
+ </TabsTrigger>
268
+ <TabsTrigger value="comments">
269
+ Comments
270
+ {card.comments && card.comments > 0 && (
271
+ <Badge variant="secondary" className="ml-2">
272
+ {card.comments}
273
+ </Badge>
274
+ )}
275
+ </TabsTrigger>
276
+ </TabsList>
277
+ </div>
278
+
279
+ <ScrollArea className="flex-1 max-h-[calc(90vh-200px)]">
280
+ <div className="p-6">
281
+ <TabsContent value="overview" className="mt-0">
282
+ <div className="space-y-6">
283
+ {/* Description */}
284
+ <div>
285
+ <Label className="text-base font-semibold mb-2 block">Description</Label>
286
+ {isEditingDescription ? (
287
+ <div className="space-y-2">
288
+ <Textarea
289
+ value={card.description || ''}
290
+ onChange={(e) => updateCard({ description: e.target.value })}
291
+ placeholder="Add a description..."
292
+ className="min-h-[100px]"
293
+ autoFocus
294
+ />
295
+ <div className="flex gap-2">
296
+ <Button
297
+ size="sm"
298
+ onClick={() => setIsEditingDescription(false)}
299
+ >
300
+ Save
301
+ </Button>
302
+ <Button
303
+ size="sm"
304
+ variant="ghost"
305
+ onClick={() => {
306
+ setCard(initialCard)
307
+ setIsEditingDescription(false)
308
+ }}
309
+ >
310
+ Cancel
311
+ </Button>
312
+ </div>
313
+ </div>
314
+ ) : (
315
+ <div
316
+ className="min-h-[50px] p-3 rounded-md border cursor-pointer hover:bg-muted/50"
317
+ onClick={() => setIsEditingDescription(true)}
318
+ >
319
+ {card.description || (
320
+ <span className="text-muted-foreground">Add a description...</span>
321
+ )}
322
+ </div>
323
+ )}
324
+ </div>
325
+
326
+ {/* Details Grid */}
327
+ <div className="grid grid-cols-2 gap-4">
328
+ {/* Assignees */}
329
+ <div>
330
+ <Label className="text-sm font-medium mb-2 block">Assignees</Label>
331
+ <Popover>
332
+ <PopoverTrigger asChild>
333
+ <Button variant="outline" className="w-full justify-start">
334
+ {card.assignees && card.assignees.length > 0 ? (
335
+ <div className="flex items-center gap-2">
336
+ <MoonUIAvatarGroupPro max={3} size="xs">
337
+ {card.assignees.map((assignee) => (
338
+ <MoonUIAvatarPro key={assignee.id} className="h-5 w-5">
339
+ <MoonUIAvatarImagePro src={assignee.avatar} />
340
+ <MoonUIAvatarFallbackPro className="text-xs">
341
+ {getInitials(assignee.name)}
342
+ </MoonUIAvatarFallbackPro>
343
+ </MoonUIAvatarPro>
344
+ ))}
345
+ </MoonUIAvatarGroupPro>
346
+ <span className="text-sm">
347
+ {card.assignees.map(a => a.name).join(', ')}
348
+ </span>
349
+ </div>
350
+ ) : (
351
+ <span className="text-muted-foreground">Add assignee</span>
352
+ )}
353
+ </Button>
354
+ </PopoverTrigger>
355
+ <PopoverContent className="p-0" align="start">
356
+ <Command>
357
+ <CommandInput placeholder="Search assignees..." />
358
+ <CommandEmpty>No assignees found.</CommandEmpty>
359
+ <CommandGroup>
360
+ {availableAssignees.map((assignee) => {
361
+ const isSelected = card.assignees?.some(a => a.id === assignee.id)
362
+ return (
363
+ <CommandItem
364
+ key={assignee.id}
365
+ onSelect={() => {
366
+ if (isSelected) {
367
+ updateCard({
368
+ assignees: card.assignees?.filter(a => a.id !== assignee.id)
369
+ })
370
+ } else {
371
+ updateCard({
372
+ assignees: [...(card.assignees || []), assignee]
373
+ })
374
+ }
375
+ }}
376
+ >
377
+ <Check
378
+ className={cn(
379
+ "mr-2 h-4 w-4",
380
+ isSelected ? "opacity-100" : "opacity-0"
381
+ )}
382
+ />
383
+ <MoonUIAvatarPro className="h-6 w-6 mr-2">
384
+ <MoonUIAvatarImagePro src={assignee.avatar} />
385
+ <MoonUIAvatarFallbackPro className="text-xs">
386
+ {getInitials(assignee.name)}
387
+ </MoonUIAvatarFallbackPro>
388
+ </MoonUIAvatarPro>
389
+ <span>{assignee.name}</span>
390
+ </CommandItem>
391
+ )
392
+ })}
393
+ </CommandGroup>
394
+ </Command>
395
+ </PopoverContent>
396
+ </Popover>
397
+ </div>
398
+
399
+ {/* Priority */}
400
+ <div>
401
+ <Label className="text-sm font-medium mb-2 block">Priority</Label>
402
+ <Select value={card.priority || 'medium'} onValueChange={(value: any) => updateCard({ priority: value })}>
403
+ <SelectTrigger>
404
+ <SelectValue />
405
+ </SelectTrigger>
406
+ <SelectContent>
407
+ {PRIORITY_OPTIONS.map((option) => (
408
+ <SelectItem key={option.value} value={option.value}>
409
+ <div className="flex items-center gap-2">
410
+ <option.icon className={cn("h-4 w-4", option.color)} />
411
+ <span>{option.label}</span>
412
+ </div>
413
+ </SelectItem>
414
+ ))}
415
+ </SelectContent>
416
+ </Select>
417
+ </div>
418
+
419
+ {/* Due Date */}
420
+ <div>
421
+ <Label className="text-sm font-medium mb-2 block">Due Date</Label>
422
+ <Popover>
423
+ <PopoverTrigger asChild>
424
+ <Button variant="outline" className="w-full justify-start">
425
+ {card.dueDate ? (
426
+ <>
427
+ <Calendar className="mr-2 h-4 w-4" />
428
+ {format(card.dueDate, 'PPP')}
429
+ </>
430
+ ) : (
431
+ <span className="text-muted-foreground">Set due date</span>
432
+ )}
433
+ </Button>
434
+ </PopoverTrigger>
435
+ <PopoverContent className="w-auto p-0" align="start">
436
+ <CalendarComponent
437
+ mode="single"
438
+ selected={card.dueDate}
439
+ onSelect={(date) => {
440
+ if (date instanceof Date) {
441
+ updateCard({ dueDate: date })
442
+ } else {
443
+ updateCard({ dueDate: undefined })
444
+ }
445
+ }}
446
+ initialFocus
447
+ />
448
+ </PopoverContent>
449
+ </Popover>
450
+ </div>
451
+
452
+ {/* Labels */}
453
+ <div>
454
+ <Label className="text-sm font-medium mb-2 block">Labels</Label>
455
+ <Popover>
456
+ <PopoverTrigger asChild>
457
+ <Button variant="outline" className="w-full justify-start">
458
+ {card.labels && card.labels.length > 0 ? (
459
+ <div className="flex flex-wrap gap-1">
460
+ {card.labels.map((label) => (
461
+ <div
462
+ key={label.id}
463
+ className="px-2 py-0.5 rounded text-xs text-white"
464
+ style={{ backgroundColor: label.color }}
465
+ >
466
+ {label.name}
467
+ </div>
468
+ ))}
469
+ </div>
470
+ ) : (
471
+ <span className="text-muted-foreground">Add labels</span>
472
+ )}
473
+ </Button>
474
+ </PopoverTrigger>
475
+ <PopoverContent className="p-3" align="start">
476
+ <div className="space-y-2">
477
+ {availableLabels.map((label) => {
478
+ const isSelected = card.labels?.some(l => l.id === label.id)
479
+ return (
480
+ <div
481
+ key={label.id}
482
+ className={cn(
483
+ "flex items-center gap-2 p-2 rounded cursor-pointer hover:bg-muted",
484
+ isSelected && "bg-muted"
485
+ )}
486
+ onClick={() => {
487
+ if (isSelected) {
488
+ updateCard({
489
+ labels: card.labels?.filter(l => l.id !== label.id)
490
+ })
491
+ } else {
492
+ updateCard({
493
+ labels: [...(card.labels || []), label]
494
+ })
495
+ }
496
+ }}
497
+ >
498
+ <div
499
+ className="w-6 h-6 rounded"
500
+ style={{ backgroundColor: label.color }}
501
+ />
502
+ <span className="text-sm">{label.name}</span>
503
+ {isSelected && <Check className="h-4 w-4 ml-auto" />}
504
+ </div>
505
+ )
506
+ })}
507
+ </div>
508
+ </PopoverContent>
509
+ </Popover>
510
+ </div>
511
+ </div>
512
+
513
+ {/* Checklist */}
514
+ <div>
515
+ <div className="flex items-center justify-between mb-3">
516
+ <Label className="text-base font-semibold">Checklist</Label>
517
+ {totalChecklistItems > 0 && (
518
+ <div className="flex items-center gap-2">
519
+ <span className="text-sm text-muted-foreground">
520
+ {completedChecklistItems}/{totalChecklistItems}
521
+ </span>
522
+ <Progress value={checklistProgress} className="w-20 h-2" />
523
+ </div>
524
+ )}
525
+ </div>
526
+ <div className="space-y-2">
527
+ {card.checklist?.items.map((item) => (
528
+ <div key={item.id} className="flex items-center gap-2 group">
529
+ <Checkbox
530
+ checked={item.completed}
531
+ onCheckedChange={() => toggleChecklistItem(item.id)}
532
+ />
533
+ <span className={cn(
534
+ "flex-1",
535
+ item.completed && "line-through text-muted-foreground"
536
+ )}>
537
+ {item.text}
538
+ </span>
539
+ <Button
540
+ variant="ghost"
541
+ size="icon"
542
+ className="h-6 w-6 opacity-0 group-hover:opacity-100"
543
+ onClick={() => deleteChecklistItem(item.id)}
544
+ >
545
+ <Trash2 className="h-3 w-3" />
546
+ </Button>
547
+ </div>
548
+ ))}
549
+ <div className="flex items-center gap-2">
550
+ <Checkbox disabled className="invisible" />
551
+ <Input
552
+ placeholder="Add an item..."
553
+ value={newChecklistItem}
554
+ onChange={(e) => setNewChecklistItem(e.target.value)}
555
+ onKeyDown={(e) => {
556
+ if (e.key === 'Enter') {
557
+ e.preventDefault()
558
+ addChecklistItem()
559
+ }
560
+ }}
561
+ />
562
+ <Button
563
+ size="sm"
564
+ onClick={addChecklistItem}
565
+ disabled={!newChecklistItem.trim()}
566
+ >
567
+ Add
568
+ </Button>
569
+ </div>
570
+ </div>
571
+ </div>
572
+
573
+ {/* Attachments */}
574
+ <div>
575
+ <Label className="text-base font-semibold mb-3 block">Attachments</Label>
576
+ <div className="space-y-2">
577
+ {card.attachments?.map((attachment) => (
578
+ <div key={attachment.id} className="flex items-center gap-3 p-3 border rounded-lg group">
579
+ <div className="flex-shrink-0">
580
+ {attachment.type.startsWith('image/') ? (
581
+ <Image className="h-8 w-8 text-muted-foreground" />
582
+ ) : (
583
+ <FileText className="h-8 w-8 text-muted-foreground" />
584
+ )}
585
+ </div>
586
+ <div className="flex-1 min-w-0">
587
+ <p className="text-sm font-medium truncate">{attachment.name}</p>
588
+ <p className="text-xs text-muted-foreground">{formatFileSize(attachment.size)}</p>
589
+ </div>
590
+ <div className="flex items-center gap-1 opacity-0 group-hover:opacity-100">
591
+ <Button
592
+ variant="ghost"
593
+ size="icon"
594
+ className="h-8 w-8"
595
+ onClick={() => window.open(attachment.url, '_blank')}
596
+ >
597
+ <Download className="h-4 w-4" />
598
+ </Button>
599
+ <Button
600
+ variant="ghost"
601
+ size="icon"
602
+ className="h-8 w-8"
603
+ onClick={() => removeAttachment(attachment.id)}
604
+ >
605
+ <Trash2 className="h-4 w-4" />
606
+ </Button>
607
+ </div>
608
+ </div>
609
+ ))}
610
+ <div>
611
+ <input
612
+ type="file"
613
+ id="file-upload"
614
+ className="hidden"
615
+ multiple
616
+ onChange={handleFileUpload}
617
+ />
618
+ <Button
619
+ variant="outline"
620
+ className="w-full"
621
+ onClick={() => document.getElementById('file-upload')?.click()}
622
+ >
623
+ <Upload className="mr-2 h-4 w-4" />
624
+ Add attachment
625
+ </Button>
626
+ </div>
627
+ </div>
628
+ </div>
629
+ </div>
630
+ </TabsContent>
631
+
632
+ <TabsContent value="activity" className="mt-0">
633
+ <div className="space-y-4">
634
+ {card.activities?.map((activity) => (
635
+ <div key={activity.id} className="flex gap-3">
636
+ <MoonUIAvatarPro className="h-8 w-8">
637
+ <MoonUIAvatarImagePro src={activity.user.avatar} />
638
+ <MoonUIAvatarFallbackPro className="text-xs">
639
+ {getInitials(activity.user.name)}
640
+ </MoonUIAvatarFallbackPro>
641
+ </MoonUIAvatarPro>
642
+ <div className="flex-1">
643
+ <div className="flex items-center gap-2 mb-1">
644
+ <span className="font-medium text-sm">{activity.user.name}</span>
645
+ <span className="text-sm text-muted-foreground">{activity.action}</span>
646
+ <span className="text-xs text-muted-foreground">
647
+ {format(activity.timestamp, 'PPp')}
648
+ </span>
649
+ </div>
650
+ {activity.details && (
651
+ <p className="text-sm text-muted-foreground">{activity.details}</p>
652
+ )}
653
+ </div>
654
+ </div>
655
+ ))}
656
+ {(!card.activities || card.activities.length === 0) && (
657
+ <div className="text-center py-8 text-muted-foreground">
658
+ <Activity className="h-8 w-8 mx-auto mb-2 opacity-50" />
659
+ <p>No activity yet</p>
660
+ </div>
661
+ )}
662
+ </div>
663
+ </TabsContent>
664
+
665
+ <TabsContent value="comments" className="mt-0">
666
+ <div className="space-y-4">
667
+ {/* Comment Input */}
668
+ <div className="flex gap-3">
669
+ <MoonUIAvatarPro className="h-8 w-8">
670
+ <MoonUIAvatarFallbackPro>U</MoonUIAvatarFallbackPro>
671
+ </MoonUIAvatarPro>
672
+ <div className="flex-1 space-y-2">
673
+ <Textarea
674
+ placeholder="Add a comment..."
675
+ value={newComment}
676
+ onChange={(e) => setNewComment(e.target.value)}
677
+ className="min-h-[80px]"
678
+ />
679
+ <div className="flex justify-end">
680
+ <Button
681
+ size="sm"
682
+ onClick={addComment}
683
+ disabled={!newComment.trim()}
684
+ >
685
+ <Send className="mr-2 h-4 w-4" />
686
+ Comment
687
+ </Button>
688
+ </div>
689
+ </div>
690
+ </div>
691
+
692
+ <Separator />
693
+
694
+ {/* Comments List */}
695
+ {card.activities?.filter(a => a.action === 'commented').map((comment) => (
696
+ <div key={comment.id} className="flex gap-3">
697
+ <MoonUIAvatarPro className="h-8 w-8">
698
+ <MoonUIAvatarImagePro src={comment.user.avatar} />
699
+ <MoonUIAvatarFallbackPro className="text-xs">
700
+ {getInitials(comment.user.name)}
701
+ </MoonUIAvatarFallbackPro>
702
+ </MoonUIAvatarPro>
703
+ <div className="flex-1">
704
+ <div className="flex items-center gap-2 mb-1">
705
+ <span className="font-medium text-sm">{comment.user.name}</span>
706
+ <span className="text-xs text-muted-foreground">
707
+ {format(comment.timestamp, 'PPp')}
708
+ </span>
709
+ </div>
710
+ <p className="text-sm">{comment.details}</p>
711
+ </div>
712
+ </div>
713
+ ))}
714
+ {!card.activities?.some(a => a.action === 'commented') && (
715
+ <div className="text-center py-8 text-muted-foreground">
716
+ <MessageCircle className="h-8 w-8 mx-auto mb-2 opacity-50" />
717
+ <p>No comments yet</p>
718
+ <p className="text-sm">Be the first to comment</p>
719
+ </div>
720
+ )}
721
+ </div>
722
+ </TabsContent>
723
+ </div>
724
+ </ScrollArea>
725
+ </Tabs>
726
+
727
+ {/* Footer Actions */}
728
+ <div className="border-t p-4 flex items-center justify-between">
729
+ <div className="flex items-center gap-2">
730
+ <DropdownMenu>
731
+ <DropdownMenuTrigger asChild>
732
+ <Button variant="outline" size="sm">
733
+ <MoreHorizontal className="h-4 w-4 mr-2" />
734
+ Actions
735
+ </Button>
736
+ </DropdownMenuTrigger>
737
+ <DropdownMenuContent align="start">
738
+ <DropdownMenuItem>
739
+ <Copy className="mr-2 h-4 w-4" />
740
+ Duplicate card
741
+ </DropdownMenuItem>
742
+ <DropdownMenuItem>
743
+ <Archive className="mr-2 h-4 w-4" />
744
+ Archive card
745
+ </DropdownMenuItem>
746
+ <DropdownMenuItem>
747
+ <Link2 className="mr-2 h-4 w-4" />
748
+ Copy link
749
+ </DropdownMenuItem>
750
+ <DropdownMenuSeparator />
751
+ <DropdownMenuItem
752
+ onClick={() => {
753
+ onDelete(card)
754
+ onClose()
755
+ }}
756
+ className="text-destructive"
757
+ >
758
+ <Trash2 className="mr-2 h-4 w-4" />
759
+ Delete card
760
+ </DropdownMenuItem>
761
+ </DropdownMenuContent>
762
+ </DropdownMenu>
763
+ </div>
764
+ <Button onClick={onClose}>Close</Button>
765
+ </div>
766
+ </DialogContent>
767
+ </Dialog>
768
+ )
769
+ }