@moontra/moonui-pro 2.0.22 → 2.1.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.
- package/dist/index.mjs +215 -214
- package/package.json +4 -2
- package/src/__tests__/use-intersection-observer.test.tsx +216 -0
- package/src/__tests__/use-local-storage.test.tsx +174 -0
- package/src/__tests__/use-pro-access.test.tsx +183 -0
- package/src/components/advanced-chart/advanced-chart.test.tsx +281 -0
- package/src/components/advanced-chart/index.tsx +412 -0
- package/src/components/advanced-forms/index.tsx +431 -0
- package/src/components/animated-button/index.tsx +202 -0
- package/src/components/calendar/event-dialog.tsx +372 -0
- package/src/components/calendar/index.tsx +557 -0
- package/src/components/color-picker/index.tsx +434 -0
- package/src/components/dashboard/index.tsx +334 -0
- package/src/components/data-table/data-table.test.tsx +187 -0
- package/src/components/data-table/index.tsx +368 -0
- package/src/components/draggable-list/index.tsx +100 -0
- package/src/components/enhanced/button.tsx +360 -0
- package/src/components/enhanced/card.tsx +272 -0
- package/src/components/enhanced/dialog.tsx +248 -0
- package/src/components/enhanced/index.ts +3 -0
- package/src/components/error-boundary/index.tsx +111 -0
- package/src/components/file-upload/file-upload.test.tsx +242 -0
- package/src/components/file-upload/index.tsx +362 -0
- package/src/components/floating-action-button/index.tsx +209 -0
- package/src/components/github-stars/index.tsx +414 -0
- package/src/components/health-check/index.tsx +441 -0
- package/src/components/hover-card-3d/index.tsx +170 -0
- package/src/components/index.ts +76 -0
- package/src/components/kanban/index.tsx +436 -0
- package/src/components/lazy-component/index.tsx +342 -0
- package/src/components/magnetic-button/index.tsx +170 -0
- package/src/components/memory-efficient-data/index.tsx +352 -0
- package/src/components/optimized-image/index.tsx +427 -0
- package/src/components/performance-debugger/index.tsx +591 -0
- package/src/components/performance-monitor/index.tsx +775 -0
- package/src/components/pinch-zoom/index.tsx +172 -0
- package/src/components/rich-text-editor/index-old-backup.tsx +443 -0
- package/src/components/rich-text-editor/index.tsx +1537 -0
- package/src/components/rich-text-editor/slash-commands-extension.ts +220 -0
- package/src/components/rich-text-editor/slash-commands.css +35 -0
- package/src/components/rich-text-editor/table-styles.css +65 -0
- package/src/components/spotlight-card/index.tsx +194 -0
- package/src/components/swipeable-card/index.tsx +100 -0
- package/src/components/timeline/index.tsx +333 -0
- package/src/components/ui/animated-button.tsx +185 -0
- package/src/components/ui/avatar.tsx +135 -0
- package/src/components/ui/badge.tsx +225 -0
- package/src/components/ui/button.tsx +221 -0
- package/src/components/ui/card.tsx +141 -0
- package/src/components/ui/checkbox.tsx +256 -0
- package/src/components/ui/color-picker.tsx +95 -0
- package/src/components/ui/dialog.tsx +332 -0
- package/src/components/ui/dropdown-menu.tsx +200 -0
- package/src/components/ui/hover-card-3d.tsx +103 -0
- package/src/components/ui/index.ts +33 -0
- package/src/components/ui/input.tsx +219 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/magnetic-button.tsx +129 -0
- package/src/components/ui/popover.tsx +183 -0
- package/src/components/ui/select.tsx +273 -0
- package/src/components/ui/separator.tsx +140 -0
- package/src/components/ui/slider.tsx +351 -0
- package/src/components/ui/spotlight-card.tsx +119 -0
- package/src/components/ui/switch.tsx +83 -0
- package/src/components/ui/tabs.tsx +195 -0
- package/src/components/ui/textarea.tsx +25 -0
- package/src/components/ui/toast.tsx +313 -0
- package/src/components/ui/tooltip.tsx +152 -0
- package/src/components/virtual-list/index.tsx +369 -0
- package/src/hooks/use-chart.ts +205 -0
- package/src/hooks/use-data-table.ts +182 -0
- package/src/hooks/use-docs-pro-access.ts +13 -0
- package/src/hooks/use-license-check.ts +65 -0
- package/src/hooks/use-subscription.ts +19 -0
- package/src/index.ts +14 -0
- package/src/lib/micro-interactions.ts +255 -0
- package/src/lib/utils.ts +6 -0
- package/src/patterns/login-form/index.tsx +276 -0
- package/src/patterns/login-form/types.ts +67 -0
- package/src/setupTests.ts +41 -0
- package/src/styles/design-system.css +365 -0
- package/src/styles/index.css +4 -0
- package/src/styles/tailwind.css +6 -0
- package/src/styles/tokens.css +453 -0
- package/src/types/moonui.d.ts +22 -0
- package/src/use-intersection-observer.tsx +154 -0
- package/src/use-local-storage.tsx +71 -0
- package/src/use-paddle.ts +138 -0
- package/src/use-performance-optimizer.ts +379 -0
- package/src/use-pro-access.ts +141 -0
- package/src/use-scroll-animation.ts +221 -0
- package/src/use-subscription.ts +37 -0
- package/src/use-toast.ts +32 -0
- package/src/utils/chart-helpers.ts +257 -0
- package/src/utils/cn.ts +69 -0
- package/src/utils/data-processing.ts +151 -0
- package/src/utils/license-guard.tsx +177 -0
- package/src/utils/license-validator.tsx +183 -0
- package/src/utils/package-guard.ts +60 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card'
|
|
5
|
+
import { Button } from '../ui/button'
|
|
6
|
+
import { Badge } from '../ui/badge'
|
|
7
|
+
import { Avatar, AvatarFallback, AvatarImage } from '../ui/avatar'
|
|
8
|
+
import {
|
|
9
|
+
Plus,
|
|
10
|
+
MoreHorizontal,
|
|
11
|
+
User,
|
|
12
|
+
Calendar,
|
|
13
|
+
MessageCircle,
|
|
14
|
+
Paperclip,
|
|
15
|
+
Edit,
|
|
16
|
+
Trash2,
|
|
17
|
+
GripVertical,
|
|
18
|
+
Lock,
|
|
19
|
+
Sparkles
|
|
20
|
+
} from 'lucide-react'
|
|
21
|
+
import { cn } from '../../lib/utils'
|
|
22
|
+
// Note: DocsProAccess should be handled by consuming application
|
|
23
|
+
import { useSubscription } from '../../hooks/use-subscription'
|
|
24
|
+
|
|
25
|
+
export interface KanbanCard {
|
|
26
|
+
id: string
|
|
27
|
+
title: string
|
|
28
|
+
description?: string
|
|
29
|
+
assignee?: {
|
|
30
|
+
name: string
|
|
31
|
+
avatar?: string
|
|
32
|
+
email?: string
|
|
33
|
+
}
|
|
34
|
+
dueDate?: Date
|
|
35
|
+
priority?: 'low' | 'medium' | 'high' | 'urgent'
|
|
36
|
+
tags?: string[]
|
|
37
|
+
attachments?: number
|
|
38
|
+
comments?: number
|
|
39
|
+
completed?: boolean
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface KanbanColumn {
|
|
43
|
+
id: string
|
|
44
|
+
title: string
|
|
45
|
+
color?: string
|
|
46
|
+
cards: KanbanCard[]
|
|
47
|
+
limit?: number
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface KanbanProps {
|
|
51
|
+
columns: KanbanColumn[]
|
|
52
|
+
onCardMove?: (cardId: string, fromColumn: string, toColumn: string, newIndex: number) => void
|
|
53
|
+
onCardClick?: (card: KanbanCard) => void
|
|
54
|
+
onCardEdit?: (card: KanbanCard) => void
|
|
55
|
+
onCardDelete?: (card: KanbanCard) => void
|
|
56
|
+
onAddCard?: (columnId: string) => void
|
|
57
|
+
onAddColumn?: () => void
|
|
58
|
+
className?: string
|
|
59
|
+
showAddColumn?: boolean
|
|
60
|
+
showCardDetails?: boolean
|
|
61
|
+
disabled?: boolean
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const PRIORITY_COLORS = {
|
|
65
|
+
low: 'bg-green-100 text-green-800 border-green-200',
|
|
66
|
+
medium: 'bg-yellow-100 text-yellow-800 border-yellow-200',
|
|
67
|
+
high: 'bg-orange-100 text-orange-800 border-orange-200',
|
|
68
|
+
urgent: 'bg-red-100 text-red-800 border-red-200'
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const PRIORITY_DOTS = {
|
|
72
|
+
low: 'bg-green-500',
|
|
73
|
+
medium: 'bg-yellow-500',
|
|
74
|
+
high: 'bg-orange-500',
|
|
75
|
+
urgent: 'bg-red-500'
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function Kanban({
|
|
79
|
+
columns,
|
|
80
|
+
onCardMove,
|
|
81
|
+
onCardClick,
|
|
82
|
+
onCardEdit,
|
|
83
|
+
onCardDelete,
|
|
84
|
+
onAddCard,
|
|
85
|
+
onAddColumn,
|
|
86
|
+
className,
|
|
87
|
+
showAddColumn = true,
|
|
88
|
+
showCardDetails = true,
|
|
89
|
+
disabled = false
|
|
90
|
+
}: KanbanProps) {
|
|
91
|
+
// Check if we're in docs mode or have pro access
|
|
92
|
+
const docsProAccess = { hasAccess: true } // Pro access assumed in package
|
|
93
|
+
const { hasProAccess, isLoading } = useSubscription()
|
|
94
|
+
|
|
95
|
+
// In docs mode, always show the component
|
|
96
|
+
const canShowComponent = docsProAccess.isDocsMode || hasProAccess
|
|
97
|
+
|
|
98
|
+
// If not in docs mode and no pro access, show upgrade prompt
|
|
99
|
+
if (!docsProAccess.isDocsMode && !isLoading && !hasProAccess) {
|
|
100
|
+
return (
|
|
101
|
+
<Card className={cn("w-full", className)}>
|
|
102
|
+
<CardContent className="py-12 text-center">
|
|
103
|
+
<div className="max-w-md mx-auto space-y-4">
|
|
104
|
+
<div className="rounded-full bg-purple-100 dark:bg-purple-900/30 p-3 w-fit mx-auto">
|
|
105
|
+
<Lock className="h-6 w-6 text-purple-600 dark:text-purple-400" />
|
|
106
|
+
</div>
|
|
107
|
+
<div>
|
|
108
|
+
<h3 className="font-semibold text-lg mb-2">Pro Feature</h3>
|
|
109
|
+
<p className="text-muted-foreground text-sm mb-4">
|
|
110
|
+
Kanban Board is available exclusively to MoonUI Pro subscribers.
|
|
111
|
+
</p>
|
|
112
|
+
<div className="flex gap-3 justify-center">
|
|
113
|
+
<a href="/pricing">
|
|
114
|
+
<Button size="sm">
|
|
115
|
+
<Sparkles className="mr-2 h-4 w-4" />
|
|
116
|
+
Upgrade to Pro
|
|
117
|
+
</Button>
|
|
118
|
+
</a>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</CardContent>
|
|
123
|
+
</Card>
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const [draggedCard, setDraggedCard] = React.useState<string | null>(null)
|
|
128
|
+
const [draggedOverColumn, setDraggedOverColumn] = React.useState<string | null>(null)
|
|
129
|
+
const [draggedFromColumn, setDraggedFromColumn] = React.useState<string | null>(null)
|
|
130
|
+
|
|
131
|
+
const handleDragStart = (e: React.DragEvent, cardId: string) => {
|
|
132
|
+
if (disabled) return
|
|
133
|
+
|
|
134
|
+
// Find which column this card belongs to
|
|
135
|
+
const sourceColumn = columns.find(col =>
|
|
136
|
+
col.cards.some(card => card.id === cardId)
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
setDraggedCard(cardId)
|
|
140
|
+
setDraggedFromColumn(sourceColumn?.id || null)
|
|
141
|
+
e.dataTransfer.effectAllowed = 'move'
|
|
142
|
+
e.dataTransfer.setData('text/plain', cardId)
|
|
143
|
+
|
|
144
|
+
// Add visual feedback
|
|
145
|
+
e.currentTarget.classList.add('opacity-50')
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const handleDragEnd = (e: React.DragEvent) => {
|
|
149
|
+
if (disabled) return
|
|
150
|
+
|
|
151
|
+
// Reset all states
|
|
152
|
+
setDraggedCard(null)
|
|
153
|
+
setDraggedOverColumn(null)
|
|
154
|
+
setDraggedFromColumn(null)
|
|
155
|
+
|
|
156
|
+
// Remove visual feedback
|
|
157
|
+
e.currentTarget.classList.remove('opacity-50')
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const handleDragOver = (e: React.DragEvent, columnId: string) => {
|
|
161
|
+
if (disabled) return
|
|
162
|
+
e.preventDefault()
|
|
163
|
+
e.dataTransfer.dropEffect = 'move'
|
|
164
|
+
setDraggedOverColumn(columnId)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const handleDragEnter = (e: React.DragEvent, columnId: string) => {
|
|
168
|
+
if (disabled) return
|
|
169
|
+
e.preventDefault()
|
|
170
|
+
setDraggedOverColumn(columnId)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const handleDragLeave = (e: React.DragEvent, columnId: string) => {
|
|
174
|
+
if (disabled) return
|
|
175
|
+
e.preventDefault()
|
|
176
|
+
|
|
177
|
+
// Only clear if we're leaving the column entirely
|
|
178
|
+
const rect = e.currentTarget.getBoundingClientRect()
|
|
179
|
+
const isLeavingColumn = (
|
|
180
|
+
e.clientX < rect.left ||
|
|
181
|
+
e.clientX > rect.right ||
|
|
182
|
+
e.clientY < rect.top ||
|
|
183
|
+
e.clientY > rect.bottom
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
if (isLeavingColumn) {
|
|
187
|
+
setDraggedOverColumn(null)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const handleDrop = (e: React.DragEvent, columnId: string) => {
|
|
192
|
+
if (disabled) return
|
|
193
|
+
e.preventDefault()
|
|
194
|
+
|
|
195
|
+
const cardId = e.dataTransfer.getData('text/plain') || draggedCard
|
|
196
|
+
|
|
197
|
+
if (cardId && onCardMove && draggedFromColumn && draggedFromColumn !== columnId) {
|
|
198
|
+
const targetColumn = columns.find(col => col.id === columnId)
|
|
199
|
+
const newIndex = targetColumn?.cards.length || 0
|
|
200
|
+
onCardMove(cardId, draggedFromColumn, columnId, newIndex)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Reset all states
|
|
204
|
+
setDraggedCard(null)
|
|
205
|
+
setDraggedOverColumn(null)
|
|
206
|
+
setDraggedFromColumn(null)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const handleCardClick = (card: KanbanCard) => {
|
|
210
|
+
if (disabled) return
|
|
211
|
+
onCardClick?.(card)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const handleCardEdit = (card: KanbanCard, e: React.MouseEvent) => {
|
|
215
|
+
if (disabled) return
|
|
216
|
+
e.stopPropagation()
|
|
217
|
+
onCardEdit?.(card)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const handleCardDelete = (card: KanbanCard, e: React.MouseEvent) => {
|
|
221
|
+
if (disabled) return
|
|
222
|
+
e.stopPropagation()
|
|
223
|
+
onCardDelete?.(card)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const formatDate = (date: Date) => {
|
|
227
|
+
return date.toLocaleDateString('en-US', {
|
|
228
|
+
month: 'short',
|
|
229
|
+
day: 'numeric'
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const isOverdue = (dueDate: Date) => {
|
|
234
|
+
return dueDate < new Date()
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const getInitials = (name: string) => {
|
|
238
|
+
return name.split(' ').map(n => n[0]).join('').toUpperCase()
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return (
|
|
242
|
+
<div className={cn("w-full", className)}>
|
|
243
|
+
<div className="flex gap-6 overflow-x-auto pb-4">
|
|
244
|
+
{columns.map((column) => {
|
|
245
|
+
const isOverLimit = column.limit && column.cards.length > column.limit
|
|
246
|
+
const isDraggedOver = draggedOverColumn === column.id
|
|
247
|
+
|
|
248
|
+
return (
|
|
249
|
+
<div
|
|
250
|
+
key={column.id}
|
|
251
|
+
className={cn(
|
|
252
|
+
"flex-shrink-0 w-80 transition-colors duration-200",
|
|
253
|
+
isDraggedOver && "bg-primary/5 rounded-lg border-2 border-primary/20"
|
|
254
|
+
)}
|
|
255
|
+
onDragOver={(e) => handleDragOver(e, column.id)}
|
|
256
|
+
onDragEnter={(e) => handleDragEnter(e, column.id)}
|
|
257
|
+
onDragLeave={(e) => handleDragLeave(e, column.id)}
|
|
258
|
+
onDrop={(e) => handleDrop(e, column.id)}
|
|
259
|
+
>
|
|
260
|
+
<Card className="h-full">
|
|
261
|
+
<CardHeader className="pb-3">
|
|
262
|
+
<div className="flex items-center justify-between">
|
|
263
|
+
<div className="flex items-center gap-2">
|
|
264
|
+
{column.color && (
|
|
265
|
+
<div
|
|
266
|
+
className="w-3 h-3 rounded-full"
|
|
267
|
+
style={{ backgroundColor: column.color }}
|
|
268
|
+
/>
|
|
269
|
+
)}
|
|
270
|
+
<CardTitle className="text-sm font-medium">
|
|
271
|
+
{column.title}
|
|
272
|
+
</CardTitle>
|
|
273
|
+
<Badge variant="secondary" className="text-xs">
|
|
274
|
+
{column.cards.length}
|
|
275
|
+
</Badge>
|
|
276
|
+
</div>
|
|
277
|
+
<Button variant="ghost" size="sm">
|
|
278
|
+
<MoreHorizontal className="h-4 w-4" />
|
|
279
|
+
</Button>
|
|
280
|
+
</div>
|
|
281
|
+
{column.limit && (
|
|
282
|
+
<CardDescription className={cn(
|
|
283
|
+
"text-xs",
|
|
284
|
+
isOverLimit && "text-destructive"
|
|
285
|
+
)}>
|
|
286
|
+
{isOverLimit ? 'Over limit' : `${column.cards.length}/${column.limit} cards`}
|
|
287
|
+
</CardDescription>
|
|
288
|
+
)}
|
|
289
|
+
</CardHeader>
|
|
290
|
+
|
|
291
|
+
<CardContent className="space-y-3">
|
|
292
|
+
{/* Cards */}
|
|
293
|
+
{column.cards.map((card) => (
|
|
294
|
+
<div
|
|
295
|
+
key={card.id}
|
|
296
|
+
draggable={!disabled}
|
|
297
|
+
onDragStart={(e) => handleDragStart(e, card.id)}
|
|
298
|
+
onDragEnd={handleDragEnd}
|
|
299
|
+
onClick={() => handleCardClick(card)}
|
|
300
|
+
className={cn(
|
|
301
|
+
"p-3 bg-background border rounded-lg cursor-pointer hover:shadow-md transition-all duration-200",
|
|
302
|
+
"group relative select-none",
|
|
303
|
+
draggedCard === card.id && "opacity-50 scale-95",
|
|
304
|
+
disabled && "cursor-not-allowed"
|
|
305
|
+
)}
|
|
306
|
+
>
|
|
307
|
+
<div className="flex items-start justify-between gap-2">
|
|
308
|
+
<div className="flex-1">
|
|
309
|
+
<h4 className="font-medium text-sm mb-1">{card.title}</h4>
|
|
310
|
+
{card.description && (
|
|
311
|
+
<p className="text-xs text-muted-foreground mb-2 line-clamp-2">
|
|
312
|
+
{card.description}
|
|
313
|
+
</p>
|
|
314
|
+
)}
|
|
315
|
+
</div>
|
|
316
|
+
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
317
|
+
<Button
|
|
318
|
+
variant="ghost"
|
|
319
|
+
size="sm"
|
|
320
|
+
className="h-6 w-6 p-0"
|
|
321
|
+
onClick={(e) => handleCardEdit(card, e)}
|
|
322
|
+
>
|
|
323
|
+
<Edit className="h-3 w-3" />
|
|
324
|
+
</Button>
|
|
325
|
+
<Button
|
|
326
|
+
variant="ghost"
|
|
327
|
+
size="sm"
|
|
328
|
+
className="h-6 w-6 p-0"
|
|
329
|
+
onClick={(e) => handleCardDelete(card, e)}
|
|
330
|
+
>
|
|
331
|
+
<Trash2 className="h-3 w-3" />
|
|
332
|
+
</Button>
|
|
333
|
+
<div className="cursor-move">
|
|
334
|
+
<GripVertical className="h-3 w-3 text-muted-foreground" />
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
|
|
339
|
+
{/* Tags */}
|
|
340
|
+
{card.tags && card.tags.length > 0 && (
|
|
341
|
+
<div className="flex flex-wrap gap-1 mb-2">
|
|
342
|
+
{card.tags.map((tag, index) => (
|
|
343
|
+
<Badge key={index} variant="outline" className="text-xs px-1 py-0">
|
|
344
|
+
{tag}
|
|
345
|
+
</Badge>
|
|
346
|
+
))}
|
|
347
|
+
</div>
|
|
348
|
+
)}
|
|
349
|
+
|
|
350
|
+
{/* Card Details */}
|
|
351
|
+
{showCardDetails && (
|
|
352
|
+
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
|
353
|
+
<div className="flex items-center gap-2">
|
|
354
|
+
{card.priority && (
|
|
355
|
+
<div className="flex items-center gap-1">
|
|
356
|
+
<div className={cn("w-2 h-2 rounded-full", PRIORITY_DOTS[card.priority])} />
|
|
357
|
+
<span className="capitalize">{card.priority}</span>
|
|
358
|
+
</div>
|
|
359
|
+
)}
|
|
360
|
+
{card.dueDate && (
|
|
361
|
+
<div className={cn(
|
|
362
|
+
"flex items-center gap-1",
|
|
363
|
+
isOverdue(card.dueDate) && "text-destructive"
|
|
364
|
+
)}>
|
|
365
|
+
<Calendar className="h-3 w-3" />
|
|
366
|
+
<span>{formatDate(card.dueDate)}</span>
|
|
367
|
+
</div>
|
|
368
|
+
)}
|
|
369
|
+
</div>
|
|
370
|
+
|
|
371
|
+
<div className="flex items-center gap-2">
|
|
372
|
+
{card.comments && card.comments > 0 && (
|
|
373
|
+
<div className="flex items-center gap-1">
|
|
374
|
+
<MessageCircle className="h-3 w-3" />
|
|
375
|
+
<span>{card.comments}</span>
|
|
376
|
+
</div>
|
|
377
|
+
)}
|
|
378
|
+
{card.attachments && card.attachments > 0 && (
|
|
379
|
+
<div className="flex items-center gap-1">
|
|
380
|
+
<Paperclip className="h-3 w-3" />
|
|
381
|
+
<span>{card.attachments}</span>
|
|
382
|
+
</div>
|
|
383
|
+
)}
|
|
384
|
+
{card.assignee && (
|
|
385
|
+
<Avatar className="h-5 w-5">
|
|
386
|
+
<AvatarImage src={card.assignee.avatar} />
|
|
387
|
+
<AvatarFallback className="text-xs">
|
|
388
|
+
{getInitials(card.assignee.name)}
|
|
389
|
+
</AvatarFallback>
|
|
390
|
+
</Avatar>
|
|
391
|
+
)}
|
|
392
|
+
</div>
|
|
393
|
+
</div>
|
|
394
|
+
)}
|
|
395
|
+
</div>
|
|
396
|
+
))}
|
|
397
|
+
|
|
398
|
+
{/* Add Card Button */}
|
|
399
|
+
{onAddCard && (
|
|
400
|
+
<Button
|
|
401
|
+
variant="ghost"
|
|
402
|
+
size="sm"
|
|
403
|
+
onClick={() => onAddCard(column.id)}
|
|
404
|
+
className="w-full justify-start text-muted-foreground hover:text-foreground"
|
|
405
|
+
disabled={disabled}
|
|
406
|
+
>
|
|
407
|
+
<Plus className="h-4 w-4 mr-2" />
|
|
408
|
+
Add card
|
|
409
|
+
</Button>
|
|
410
|
+
)}
|
|
411
|
+
</CardContent>
|
|
412
|
+
</Card>
|
|
413
|
+
</div>
|
|
414
|
+
)
|
|
415
|
+
})}
|
|
416
|
+
|
|
417
|
+
{/* Add Column Button */}
|
|
418
|
+
{showAddColumn && onAddColumn && (
|
|
419
|
+
<div className="flex-shrink-0 w-80">
|
|
420
|
+
<Button
|
|
421
|
+
variant="outline"
|
|
422
|
+
onClick={onAddColumn}
|
|
423
|
+
className="w-full h-full min-h-[200px] border-dashed justify-center items-center"
|
|
424
|
+
disabled={disabled}
|
|
425
|
+
>
|
|
426
|
+
<Plus className="h-6 w-6 mr-2" />
|
|
427
|
+
Add column
|
|
428
|
+
</Button>
|
|
429
|
+
</div>
|
|
430
|
+
)}
|
|
431
|
+
</div>
|
|
432
|
+
</div>
|
|
433
|
+
)
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export default Kanban
|